Added first files
Some checks failed
CI / build (push) Failing after 51s

This commit is contained in:
Andrey Kernichniy 2026-03-08 00:30:02 +07:00
parent 4f9a581530
commit 17f580fc70
15 changed files with 296 additions and 3 deletions

View file

@ -41,7 +41,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: gxc-math-${{ github.sha }}
name: gxc-command-executer-${{ github.sha }}
path: ./dist/
deploy:
@ -53,7 +53,7 @@ jobs:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: gxc-math-${{ github.sha }}
name: gxc-command-executer-${{ github.sha }}
path: ./artifact
- name: Setup Node.js

View file

@ -0,0 +1,28 @@
import { isIgnorableKey, isImmutableKey } from "@gxc-solutions/model/decorators";
import { Subject } from "rxjs";
import { ICommand, ICommandExecuter, ICommandResult } from "../interfaces";
import { IModel } from "../interfaces/model";
export abstract class AbstractExecutor<T extends IModel> implements ICommandExecuter<T> {
protected _changes$ = new Subject<ICommandResult<T>[]>();
readonly changes$ = this._changes$.asObservable();
abstract execute(command: ICommand<T>, object: T): this;
abstract flush(): Promise<void>;
protected _apply(object: T, values: Partial<T>): boolean {
return Object.keys(values).some((key) => {
if (typeof object[key] === "object" && object[key] != null && !isImmutableKey(object, key)) {
return this._apply(object[key], values[key]);
} else if (isIgnorableKey(object, key)) {
return false;
} else if (object[key] === values[key]) {
return false;
} else {
object[key] = values[key];
return true;
}
});
}
}

View file

@ -0,0 +1,27 @@
import { IModel } from "@gxc-solutions/model/interfaces";
import { IIndexer } from "../interfaces";
import { ParallelCommandExecutor } from "./parallel-executor";
import { Queue } from "./queue";
import { SerialExecutor } from "./serial-executor";
export class CommandsExecutor<T extends IModel> {
private _queue = new Queue();
private _parallel: ParallelCommandExecutor<T>;
private _serial: SerialExecutor<T>;
constructor(private _indexer: IIndexer<T>) {
this._parallel = new ParallelCommandExecutor<T>(this._indexer);
this._serial = new SerialExecutor<T>(this._indexer);
}
async executeParallel() {
while (!this._queue.isEmpty()) {
// const flush = this._queue.dequeue();
// const result = await flush;
}
const result = this._parallel.flush();
this._queue.enqueue(result);
}
executeIndependent() {}
}

View file

@ -0,0 +1,62 @@
import { ICommand, ICommandResult, IIndexer, IModel } from "../interfaces";
import { AbstractExecutor } from "./abstract-executor";
import { Queue } from "./queue";
// Set of actions for apply once
export class ParallelCommandExecutor<T extends IModel> extends AbstractExecutor<T> {
private _results: Promise<ICommandResult<T>[]>[] = [];
private _queue: Queue<any>;
constructor(private _indexer: IIndexer<T>) {
super();
}
execute(command: ICommand<T>, object: T): this {
const result = command.execute(object);
this._results.push(result);
return this;
}
async flush() {
if (this._results.length > 0) {
const allResults = await Promise.all(this._results);
const isWrongResult = this._checkResults(allResults);
if (isWrongResult) {
this._results = [];
throw new Error("Commands changed the same object multiple times. This is not allowed.");
}
const flattenResults = allResults.flat();
const isChanged = flattenResults.some(({ id, result }) => {
const object = this._indexer.get(id);
return this._apply(object, result);
});
if (!isChanged) {
this._results = [];
return;
}
this._changes$.next(flattenResults);
}
this._results = [];
}
private _checkResults(results: ICommandResult<T>[][]): boolean {
let isWrongResults = false;
const uniqIds = new Set<string>();
for (const commandResult of results) {
for (const objectResult of commandResult) {
if (uniqIds.has(objectResult.id)) {
isWrongResults = true;
break;
} else {
uniqIds.add(objectResult.id);
}
}
}
return isWrongResults;
}
}

View file

@ -0,0 +1,27 @@
export class Queue<T> {
private _items: T[] = [];
get length() {
return this._items.length;
}
enqueue(element: T): void {
this._items.push(element);
}
dequeue(): T | undefined {
return this._items.shift();
}
peek(): T | undefined {
return this._items[0];
}
isEmpty(): boolean {
return this._items.length === 0;
}
clear(): void {
this._items = [];
}
}

View file

@ -0,0 +1,42 @@
import { ICommand, IIndexer, IModel } from "../interfaces";
import { AbstractExecutor } from "./abstract-executor";
interface ICommandAndArg<T extends IModel> {
command: ICommand<T>;
object: T;
}
// Set of actions for serial execution
export class SerialExecutor<T extends IModel> extends AbstractExecutor<T> {
private _commands: ICommandAndArg<T>[] = [];
private _isFlushed = false;
constructor(private _indexer: IIndexer<T>) {
super();
}
execute(command: ICommand<T>, object: T): this {
if (this._isFlushed) {
throw new Error("Cannot execute commands after flush has been called.");
}
this._commands.push({ command, object });
return this;
}
async flush(): Promise<void> {
this._isFlushed = true;
for (const { command, object } of this._commands) {
const commandResult = await command.execute(object);
const isChanged = commandResult.map(({ id, result, /*type */ }) => {
const object = this._indexer.get(id);
return this._apply(object, result);
});
if (isChanged) {
this._changes$.next(commandResult);
}
}
this._commands = [];
this._isFlushed = false;
}
}

View file

@ -0,0 +1,47 @@
import { ICommand, ICommandResult, IIndexer, IModel } from "../interfaces";
import { AbstractExecutor } from "./abstract-executor";
interface ICommandAndArg<T extends IModel> {
command: ICommand<T>;
object: T;
}
// Set of actions for serial execution
export class SerialExecutor<T extends IModel> extends AbstractExecutor<T> {
private _commands: ICommandAndArg<T>[] = [];
constructor(private _indexer: IIndexer<T>) {
super();
}
execute(command: ICommand<T>, object: T): this {
this._commands.push({ command, object });
return this;
}
async flush(): Promise<void> {
const results = new Map<string, ICommandResult<T>>();
for (const { command, object } of this._commands) {
const commandResult = await command.execute(object);
commandResult.map(({ id, result, type }) => {
if (results.has(id)) {
const previousResults = results.get(id);
results.set(id, { id, type, result: { ...previousResults.result, ...result } });
} else {
results.set(id, { id, result, type });
}
});
}
const isChanged = Array.from(results.entries()).some(([id, result]) => {
const object = this._indexer.get(id);
return this._apply(object, result.result);
});
if (!isChanged) {
this._commands = [];
return;
}
this._changes$.next(Array.from(results.values()));
}
}

View file

@ -0,0 +1,18 @@
import { Observable } from "rxjs";
import { IModel } from "./model";
export interface ICommandExecuter<T extends IModel> {
changes$: Observable<ICommandResult<T>[]>;
execute(command: ICommand<T>, object: T): this;
flush(): Promise<void>;
}
export interface ICommand<T extends IModel> {
execute(object: T): Promise<ICommandResult<T>[]>;
}
export interface ICommandResult<T extends IModel> {
id: string;
type: string;
result: Partial<T>;
}

View file

@ -0,0 +1,3 @@
export * from "./executer";
export * from "./indexer";
export * from "./model";

View file

@ -0,0 +1,6 @@
import { IModel } from "./model";
export interface IIndexer<T extends IModel> {
index(page: T): void;
get(id: string): T;
}

View file

@ -0,0 +1,6 @@
export type TypeOfModel = "collection";
export interface IModel {
readonly id: string;
readonly type: TypeOfModel;
}

View file

@ -1,10 +1,13 @@
{
"name": "@gxc-solutions/lib",
"name": "@gxc-solutions/command-executer",
"version": "0.0.1",
"main": "index.js",
"author": "GXC Solutions",
"publishConfig": {
"access": "public",
"registry": "https://npm.gxc-solutions.ru"
},
"peerDependencies": {
"rxjs": "^7.8.2"
}
}

View file

@ -0,0 +1,3 @@
# Description
Module includes classes and interfaces for execute commands

18
package-lock.json generated
View file

@ -8,6 +8,9 @@
"name": "template-of-lib-repo",
"version": "0.0.0",
"license": "ISC",
"dependencies": {
"rxjs": "^7.8.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.4",
"@eslint/js": "^10.0.1",
@ -2331,6 +2334,15 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@ -2420,6 +2432,12 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View file

@ -29,5 +29,8 @@
"rimraf": "^6.0.1",
"typescript": "^5.9.3",
"vite": "^7.3.1"
},
"dependencies": {
"rxjs": "^7.8.2"
}
}