From 17f580fc70c3373bb370b52f67b2a87a6feb521f Mon Sep 17 00:00:00 2001 From: Andrey Kernichniy Date: Sun, 8 Mar 2026 00:30:02 +0700 Subject: [PATCH] Added first files --- .forgejo/workflows/release.yml | 4 +- lib/src/executers/abstract-executor.ts | 28 ++++++++++++ lib/src/executers/executor.ts | 27 +++++++++++ lib/src/executers/parallel-executor.ts | 62 ++++++++++++++++++++++++++ lib/src/executers/queue.ts | 27 +++++++++++ lib/src/executers/serial-executor-2.ts | 42 +++++++++++++++++ lib/src/executers/serial-executor.ts | 47 +++++++++++++++++++ lib/src/interfaces/executer.ts | 18 ++++++++ lib/src/interfaces/index.ts | 3 ++ lib/src/interfaces/indexer.ts | 6 +++ lib/src/interfaces/model.ts | 6 +++ lib/src/package.json | 5 ++- lib/src/readme.md | 3 ++ package-lock.json | 18 ++++++++ package.json | 3 ++ 15 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 lib/src/executers/abstract-executor.ts create mode 100644 lib/src/executers/executor.ts create mode 100644 lib/src/executers/parallel-executor.ts create mode 100644 lib/src/executers/queue.ts create mode 100644 lib/src/executers/serial-executor-2.ts create mode 100644 lib/src/executers/serial-executor.ts create mode 100644 lib/src/interfaces/executer.ts create mode 100644 lib/src/interfaces/indexer.ts create mode 100644 lib/src/interfaces/model.ts diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 6bdf090..73f950b 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -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 diff --git a/lib/src/executers/abstract-executor.ts b/lib/src/executers/abstract-executor.ts new file mode 100644 index 0000000..276281b --- /dev/null +++ b/lib/src/executers/abstract-executor.ts @@ -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 implements ICommandExecuter { + protected _changes$ = new Subject[]>(); + readonly changes$ = this._changes$.asObservable(); + + abstract execute(command: ICommand, object: T): this; + + abstract flush(): Promise; + + protected _apply(object: T, values: Partial): 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; + } + }); + } +} diff --git a/lib/src/executers/executor.ts b/lib/src/executers/executor.ts new file mode 100644 index 0000000..2972a81 --- /dev/null +++ b/lib/src/executers/executor.ts @@ -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 { + private _queue = new Queue(); + private _parallel: ParallelCommandExecutor; + private _serial: SerialExecutor; + + constructor(private _indexer: IIndexer) { + this._parallel = new ParallelCommandExecutor(this._indexer); + this._serial = new SerialExecutor(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() {} +} diff --git a/lib/src/executers/parallel-executor.ts b/lib/src/executers/parallel-executor.ts new file mode 100644 index 0000000..9ecfcc8 --- /dev/null +++ b/lib/src/executers/parallel-executor.ts @@ -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 extends AbstractExecutor { + private _results: Promise[]>[] = []; + private _queue: Queue; + + constructor(private _indexer: IIndexer) { + super(); + } + + execute(command: ICommand, 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[][]): boolean { + let isWrongResults = false; + const uniqIds = new Set(); + + 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; + } +} diff --git a/lib/src/executers/queue.ts b/lib/src/executers/queue.ts new file mode 100644 index 0000000..faafde0 --- /dev/null +++ b/lib/src/executers/queue.ts @@ -0,0 +1,27 @@ +export class Queue { + 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 = []; + } +} diff --git a/lib/src/executers/serial-executor-2.ts b/lib/src/executers/serial-executor-2.ts new file mode 100644 index 0000000..151afc7 --- /dev/null +++ b/lib/src/executers/serial-executor-2.ts @@ -0,0 +1,42 @@ +import { ICommand, IIndexer, IModel } from "../interfaces"; +import { AbstractExecutor } from "./abstract-executor"; + +interface ICommandAndArg { + command: ICommand; + object: T; +} + +// Set of actions for serial execution +export class SerialExecutor extends AbstractExecutor { + private _commands: ICommandAndArg[] = []; + private _isFlushed = false; + + constructor(private _indexer: IIndexer) { + super(); + } + + execute(command: ICommand, 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 { + 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; + } +} diff --git a/lib/src/executers/serial-executor.ts b/lib/src/executers/serial-executor.ts new file mode 100644 index 0000000..38bc820 --- /dev/null +++ b/lib/src/executers/serial-executor.ts @@ -0,0 +1,47 @@ +import { ICommand, ICommandResult, IIndexer, IModel } from "../interfaces"; +import { AbstractExecutor } from "./abstract-executor"; + +interface ICommandAndArg { + command: ICommand; + object: T; +} + +// Set of actions for serial execution +export class SerialExecutor extends AbstractExecutor { + private _commands: ICommandAndArg[] = []; + + constructor(private _indexer: IIndexer) { + super(); + } + + execute(command: ICommand, object: T): this { + this._commands.push({ command, object }); + return this; + } + + async flush(): Promise { + const results = new Map>(); + + 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())); + } +} diff --git a/lib/src/interfaces/executer.ts b/lib/src/interfaces/executer.ts new file mode 100644 index 0000000..d559241 --- /dev/null +++ b/lib/src/interfaces/executer.ts @@ -0,0 +1,18 @@ +import { Observable } from "rxjs"; +import { IModel } from "./model"; + +export interface ICommandExecuter { + changes$: Observable[]>; + execute(command: ICommand, object: T): this; + flush(): Promise; +} + +export interface ICommand { + execute(object: T): Promise[]>; +} + +export interface ICommandResult { + id: string; + type: string; + result: Partial; +} diff --git a/lib/src/interfaces/index.ts b/lib/src/interfaces/index.ts index e69de29..70d4725 100644 --- a/lib/src/interfaces/index.ts +++ b/lib/src/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from "./executer"; +export * from "./indexer"; +export * from "./model"; diff --git a/lib/src/interfaces/indexer.ts b/lib/src/interfaces/indexer.ts new file mode 100644 index 0000000..451cbb6 --- /dev/null +++ b/lib/src/interfaces/indexer.ts @@ -0,0 +1,6 @@ +import { IModel } from "./model"; + +export interface IIndexer { + index(page: T): void; + get(id: string): T; +} diff --git a/lib/src/interfaces/model.ts b/lib/src/interfaces/model.ts new file mode 100644 index 0000000..ccf1c18 --- /dev/null +++ b/lib/src/interfaces/model.ts @@ -0,0 +1,6 @@ +export type TypeOfModel = "collection"; + +export interface IModel { + readonly id: string; + readonly type: TypeOfModel; +} diff --git a/lib/src/package.json b/lib/src/package.json index f576691..0487f5f 100644 --- a/lib/src/package.json +++ b/lib/src/package.json @@ -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" } } diff --git a/lib/src/readme.md b/lib/src/readme.md index e69de29..2d24e51 100644 --- a/lib/src/readme.md +++ b/lib/src/readme.md @@ -0,0 +1,3 @@ +# Description + +Module includes classes and interfaces for execute commands diff --git a/package-lock.json b/package-lock.json index e92ee18..6a035f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 23fa27e..e44d720 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,8 @@ "rimraf": "^6.0.1", "typescript": "^5.9.3", "vite": "^7.3.1" + }, + "dependencies": { + "rxjs": "^7.8.2" } }