Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remote context #203

Merged
merged 7 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/context/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {AlwatrObservable} from '@alwatr/observable';
import {AlwatrObservable, type AlwatrObservableConfig} from '@alwatr/observable';

import type {} from '@alwatr/nano-build';
import type {Dictionary} from '@alwatr/type-helper';
Expand All @@ -11,7 +11,7 @@ definePackage('@alwatr/context', __package_version__);
* Alwatr Context.
*/
export class AlwatrContext<T extends Dictionary> extends AlwatrObservable<T> {
constructor(config: {name: string; loggerPrefix?: string}) {
constructor(config: AlwatrObservableConfig) {
config.loggerPrefix ??= 'context-signal';
super(config);
}
Expand Down
57 changes: 32 additions & 25 deletions packages/fetch-state-machine/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {fetch, type FetchOptions} from '@alwatr/fetch';
import {AlwatrFluxStateMachineBase, type StateRecord, type ActionRecord} from '@alwatr/fsm';
import {AlwatrFluxStateMachineBase, type StateRecord, type ActionRecord, type AlwatrFluxStateMachineConfig} from '@alwatr/fsm';
import {definePackage} from '@alwatr/logger';

import type {} from '@alwatr/nano-build';
Expand All @@ -11,12 +11,16 @@ export type ServerRequestEvent = 'request' | 'requestFailed' | 'requestSuccess';

export type {FetchOptions};

export interface AlwatrFetchStateMachineConfig<S extends string> extends AlwatrFluxStateMachineConfig<S> {
fetch: Partial<FetchOptions>;
}

export abstract class AlwatrFetchStateMachineBase<
ExtraState extends string = never,
ExtraEvent extends string = never,
> extends AlwatrFluxStateMachineBase<ServerRequestState | ExtraState, ServerRequestEvent | ExtraEvent> {
protected config_: Partial<FetchOptions>;
protected fetchOptions_?: FetchOptions;
protected baseFetchOptions_: Partial<FetchOptions>;
protected currentFetchOptions_?: FetchOptions;
protected rawResponse_?: Response;

protected override stateRecord_ = {
Expand All @@ -39,20 +43,21 @@ export abstract class AlwatrFetchStateMachineBase<
_on_loading_enter: this.requestAction_,
} as ActionRecord<ServerRequestState | ExtraState, ServerRequestEvent | ExtraEvent>;

constructor(config: Partial<FetchOptions> & {name: string}) {
super({name: config.name, initialState: 'initial'});
this.config_ = config;
constructor(config: AlwatrFetchStateMachineConfig<ServerRequestState | ExtraState>) {
config.loggerPrefix ??= 'fetch-state-machine';
super(config);
this.baseFetchOptions_ = config.fetch;
}

protected request_(options?: Partial<FetchOptions>): void {
this.logger_.logMethodArgs?.('request_', options);
this.setOptions_(options);
protected request_(fetchOptions?: Partial<FetchOptions>): void {
this.logger_.logMethodArgs?.('request_', fetchOptions);
this.setFetchOptions_(fetchOptions);
this.transition_('request');
}

protected async fetch_(options: FetchOptions): Promise<void> {
this.logger_.logMethodArgs?.('fetch_', options);
this.rawResponse_ = await fetch(options);
protected async fetch_(fetchOptions: FetchOptions): Promise<void> {
this.logger_.logMethodArgs?.('fetch_', fetchOptions);
this.rawResponse_ = await fetch(fetchOptions);

if (!this.rawResponse_.ok) {
throw new Error('fetch_nok');
Expand All @@ -63,37 +68,39 @@ export abstract class AlwatrFetchStateMachineBase<
this.logger_.logMethod?.('requestAction__');

try {
if (this.fetchOptions_ === undefined) {
if (this.currentFetchOptions_ === undefined) {
throw new Error('invalid_fetch_options');
}

await this.fetch_(this.fetchOptions_);
await this.fetch_(this.currentFetchOptions_);

this.transition_('requestSuccess');
}
catch (err) {
this.logger_.error('requestAction__', 'fetch_failed', err);
this.transition_('requestFailed');
catch (error) {
this.requestFailed_(error as Error);
}
}

protected setOptions_(options?: Partial<FetchOptions>): void {
protected requestFailed_(error: Error): void {
this.logger_.error('requestFailed_', 'fetch_failed', error);
this.transition_('requestFailed');
}

protected setFetchOptions_(options?: Partial<FetchOptions>): void {
this.logger_.logMethodArgs?.('setOptions_', {options});

const fetchOptions = {
...this.config_,
this.currentFetchOptions_ = {
...this.baseFetchOptions_,
...options,
queryParams: {
...this.config_.queryParams,
...this.baseFetchOptions_.queryParams,
...options?.queryParams,
},
};
} as FetchOptions;

if (fetchOptions.url === undefined) {
if (this.currentFetchOptions_.url === undefined) {
throw new Error('invalid_fetch_options');
}

this.fetchOptions_ = fetchOptions as FetchOptions;
}

protected override resetToInitialState_(): void {
Expand Down
6 changes: 3 additions & 3 deletions packages/fetch-state-machine/src/jfsm-base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AlwatrFetchStateMachineBase, type FetchOptions } from './base.js';
import {AlwatrFetchStateMachineBase, type FetchOptions} from './base.js';

import type { JsonObject } from "@alwatr/type-helper";
import type {Json} from '@alwatr/type-helper';

export abstract class AlwatrJsonFetchStateMachineBase<
T extends JsonObject = JsonObject,
T extends Json = Json,
ExtraState extends string = never,
ExtraEvent extends string = never,
> extends AlwatrFetchStateMachineBase<ExtraState, ExtraEvent> {
Expand Down
8 changes: 4 additions & 4 deletions packages/fetch-state-machine/src/jfsm.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AlwatrJsonFetchStateMachineBase } from './jfsm-base.js';
import {AlwatrJsonFetchStateMachineBase} from './jfsm-base.js';

import type { FetchOptions, ServerRequestState } from './base.js';
import type { JsonObject } from '@alwatr/type-helper';
import type {FetchOptions, ServerRequestState} from './base.js';
import type {Json} from '@alwatr/type-helper';

export class AlwatrJsonFetchStateMachine<T extends JsonObject = JsonObject> extends AlwatrJsonFetchStateMachineBase<T> {
export class AlwatrJsonFetchStateMachine<T extends Json = Json> extends AlwatrJsonFetchStateMachineBase<T> {
/**
* Current state.
*/
Expand Down
11 changes: 8 additions & 3 deletions packages/fsm/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {definePackage} from '@alwatr/logger';
import {AlwatrObservable} from '@alwatr/observable';
import {AlwatrObservable, type AlwatrObservableConfig} from '@alwatr/observable';
import '@alwatr/polyfill-has-own';

import type {ActionName, ActionRecord, StateEventDetail, StateRecord} from './type.js';
Expand All @@ -8,6 +8,11 @@ import type {MaybePromise} from '@alwatr/type-helper';

definePackage('@alwatr/fsm', __package_version__);

export interface AlwatrFluxStateMachineConfig<S extends string> extends AlwatrObservableConfig {
initialState: S;
}


/**
* Flux (Finite) State Machine Base Class
*/
Expand All @@ -26,8 +31,8 @@ export abstract class AlwatrFluxStateMachineBase<S extends string, E extends str

protected override message_: {state: S};

constructor(config: {name: string; loggerPrefix?: string; initialState: S}) {
config.loggerPrefix ??= 'fsm';
constructor(config: AlwatrFluxStateMachineConfig<S>) {
config.loggerPrefix ??= 'flux-state-machine';
super(config);
this.message_ = {state: this.initialState_ = config.initialState};
}
Expand Down
7 changes: 6 additions & 1 deletion packages/observable/src/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import type {Dictionary} from '@alwatr/type-helper';

definePackage('@alwatr/observable', __package_version__);

export interface AlwatrObservableConfig {
name: string;
loggerPrefix?: string;
}

export abstract class AlwatrObservable<T extends Dictionary = Dictionary> implements AlwatrObservableInterface<T> {
protected name_;
protected logger_;
protected message_?: T;
protected observers__: Observer<this, T>[] = [];

constructor(config: {name: string; loggerPrefix?: string}) {
constructor(config: AlwatrObservableConfig) {
config.loggerPrefix ??= 'signal';
this.name_ = config.name;
this.logger_ = createLogger(`{${config.loggerPrefix}: ${this.name_}}`);
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "@alwatr/server-context",
"name": "@alwatr/remote-context",
"version": "1.2.6",
"description": "Elegant powerful context manager base on alwatr signal, written in tiny TypeScript, ES module.",
"author": "S. Ali Mihandoost <[email protected]> (https://ali.mihandoost.com)",
"keywords": [
"context",
"remote-context",
"server-context",
"flux",
"signal",
"typescript",
"esm",
Expand Down Expand Up @@ -33,9 +35,9 @@
"repository": {
"type": "git",
"url": "https://github.com/Alwatr/flux",
"directory": "packages/server-context"
"directory": "packages/remote-context"
},
"homepage": "https://github.com/Alwatr/flux/tree/next/packages/server-context#readme",
"homepage": "https://github.com/Alwatr/flux/tree/next/packages/remote-context#readme",
"bugs": {
"url": "https://github.com/Alwatr/flux/issues"
},
Expand All @@ -57,10 +59,8 @@
"clean": "rm -rfv dist *.tsbuildinfo"
},
"dependencies": {
"@alwatr/fetch": "^4.0.0",
"@alwatr/fsm": "workspace:^",
"@alwatr/logger": "^3.2.13",
"@alwatr/signal": "workspace:^"
"@alwatr/fetch-state-machine": "workspace:^",
"@alwatr/logger": "^3.2.13"
},
"devDependencies": {
"@alwatr/nano-build": "^1.3.9",
Expand Down
119 changes: 119 additions & 0 deletions packages/remote-context/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
AlwatrJsonFetchStateMachineBase,
type AlwatrFetchStateMachineConfig,
type ServerRequestEvent,
type ServerRequestState,
} from '@alwatr/fetch-state-machine';

import type {Json} from '@alwatr/type-helper';

type ExtraState = 'offlineCheck' | 'reloading' | 'reloadingFailed';
export type ServerContextState = ServerRequestState | ExtraState;

type ExtraEvent = 'cacheNotFound';
export type ServerContextEvent = ServerRequestEvent | ExtraEvent;

export type AlwatrRemoteContextStateMachineConfig = AlwatrFetchStateMachineConfig<ServerContextState>;

export abstract class AlwatrRemoteContextStateMachineBase<T extends Json = Json> extends AlwatrJsonFetchStateMachineBase<
T,
ExtraState,
ExtraEvent
> {
protected context_?: T;

constructor(config: AlwatrRemoteContextStateMachineConfig) {
super(config);

this.stateRecord_ = {
initial: {
request: 'offlineCheck',
},
/**
* Just check offline cache data before online request.
*/
offlineCheck: {
requestFailed: 'failed',
cacheNotFound: 'loading',
requestSuccess: 'reloading',
},
/**
* First loading without any cached context.
*/
loading: {
requestFailed: 'failed',
requestSuccess: 'complete',
},
/**
* First loading failed without any cached context.
*/
failed: {
request: 'loading', // //TODO: why offlineCheck? should be loading!
},
reloading: {
requestFailed: 'reloadingFailed',
requestSuccess: 'complete',
},
/**
* Reloading failed with previously cached context exist.
*/
reloadingFailed: {
request: 'reloading',
},
complete: {
request: 'reloading',
},
};

this.actionRecord_ = {
_on_offlineCheck_enter: this.offlineRequestAction_,
_on_loading_enter: this.onlineRequestAction_,
_on_reloading_enter: this.onlineRequestAction_,
_on_requestSuccess: this.updateContextAction_,
};
}

protected offlineRequestAction_(): void {
this.logger_.logMethod?.('offlineRequestAction_');
this.currentFetchOptions_!.cacheStrategy = 'cache_only';
this.requestAction_();
}

protected onlineRequestAction_(): void {
this.logger_.logMethod?.('onlineRequestAction_');
this.currentFetchOptions_!.cacheStrategy = 'update_cache';
this.requestAction_();
}

protected updateContextAction_(): void {
this.logger_.logMethod?.('updateContextAction_');

if (this.jsonResponse_ === undefined) {
this.logger_.accident('updateContextAction_', 'no_response_json');
return;
}

if (this.context_ !== undefined && JSON.stringify(this.context_) !== JSON.stringify(this.jsonResponse_)) {
// TODO: improve performance. compare hash value or updatedAt or catch response text.
this.context_ = this.jsonResponse_;
}

this.cleanup_();
}

protected override requestFailed_(error: Error): void {
this.logger_.logMethod?.('requestFailed_');

if (error.message === 'fetch_cache_not_found') {
this.transition_('cacheNotFound');
}
else {
super.requestFailed_(error);
}
}

protected cleanup_(): void {
delete this.rawResponse_;
delete this.jsonResponse_;
}
}
2 changes: 2 additions & 0 deletions packages/remote-context/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './base.js';
export * from './remote-context.js';
21 changes: 21 additions & 0 deletions packages/remote-context/src/remote-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {AlwatrRemoteContextStateMachineBase, type ServerContextState} from './base.js';

import type {FetchOptions} from '@alwatr/fetch-state-machine';
import type {Json} from '@alwatr/type-helper';

export class AlwatrRemoteContextStateMachine<T extends Json = Json> extends AlwatrRemoteContextStateMachineBase<T> {
/**
* Current state.
*/
get state(): ServerContextState {
return this.message_.state;
}

get context(): T | undefined {
return this.context_;
}

request(fetchOptions?: Partial<FetchOptions>): void {
return this.request_(fetchOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"composite": true,
},
"include": ["src/**/*.ts"],
"references": [{"path": "../signal"}, {"path": "../fsm"}]
"references": [{"path": "../fetch-state-machine"}]
}
3 changes: 0 additions & 3 deletions packages/server-context/src/main.ts

This file was deleted.

Loading
Loading