diff --git a/packages/pipeline/README.md b/packages/pipeline/README.md new file mode 100644 index 0000000..1140580 --- /dev/null +++ b/packages/pipeline/README.md @@ -0,0 +1,6 @@ +# @synstack/pipeline + +Functional chainable pipelines for synchronous and asynchronous functions. + +This package lets you compose a sequence of transformations while preserving +whether the resulting value is a `Promise` or not. diff --git a/packages/pipeline/package.json b/packages/pipeline/package.json new file mode 100644 index 0000000..f152f14 --- /dev/null +++ b/packages/pipeline/package.json @@ -0,0 +1,57 @@ +{ + "name": "@synstack/pipeline", + "type": "module", + "publishConfig": { + "access": "public" + }, + "version": "1.0.0", + "description": "Composable function pipelines with type safety", + "keywords": [ + "pipeline", + "functional", + "compose", + "typescript" + ], + "author": { + "name": "pAIrprog", + "url": "https://pairprog.io" + }, + "homepage": "https://github.com/pAIrprogio/synscript/tree/main/packages/pipeline", + "repository": { + "type": "git", + "url": "https://github.com/pAIrprogio/synscript.git", + "directory": "packages/pipeline" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "test:types": "tsc --noEmit", + "test:unit": "node --experimental-strip-types --experimental-test-snapshots --no-warnings --test src/**/*.test.ts", + "test:unit:watch": "node --experimental-strip-types --experimental-test-snapshots --no-warnings --watch --test --watch src/**/*.test.ts", + "test": "yarn test:types && yarn test:unit", + "prepare": "yarn test && yarn build" + }, + "exports": { + ".": { + "import": { + "types": "./dist/pipeline.index.d.ts", + "default": "./dist/pipeline.index.js" + }, + "require": { + "types": "./dist/pipeline.index.d.cts", + "default": "./dist/pipeline.index.cjs" + } + } + }, + "devDependencies": { + "@types/node": "^22.15.18", + "tsup": "^8.4.0", + "typescript": "^5.8.3" + }, + "files": [ + "src/**/*.ts", + "!src/**/*.test.ts", + "dist/**/*" + ] +} diff --git a/packages/pipeline/src/pipeline.index.ts b/packages/pipeline/src/pipeline.index.ts new file mode 100644 index 0000000..3bc2363 --- /dev/null +++ b/packages/pipeline/src/pipeline.index.ts @@ -0,0 +1 @@ +export { pipeline, Pipeline } from "./pipeline.lib.ts"; diff --git a/packages/pipeline/src/pipeline.lib.test.ts b/packages/pipeline/src/pipeline.lib.test.ts new file mode 100644 index 0000000..8d4c6f1 --- /dev/null +++ b/packages/pipeline/src/pipeline.lib.test.ts @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { assertType } from "../../shared/src/ts.utils.ts"; +import { pipeline } from "./pipeline.lib.ts"; + +const _types = () => { + const syncPipeline = pipeline._((a: number) => a + 2)._((a) => a * 2); + assertType(syncPipeline.apply(1)); + assertType(syncPipeline.$(1)); + + const asyncPipeline = pipeline + ._((a: number) => a + 2) + ._((a) => Promise.resolve(a * 2)); + assertType>(asyncPipeline.apply(1)); + assertType>(asyncPipeline.$(1)); +}; + +describe("pipeline", () => { + it("chains synchronous functions", () => { + const p = pipeline._((a: number) => a + 2)._((a) => a * 2); + assert.equal(p.apply(2), 8); + assert.equal(p.$(3), 10); + }); + + it("handles async functions", async () => { + const p = pipeline + ._((a: number) => a + 2) + ._((a) => Promise.resolve(a * 2)); + const res = p.apply(2); + assert.equal(res instanceof Promise, true); + assert.equal(await res, 8); + }); + + it("mixes async and sync steps", async () => { + const p = pipeline + ._((a: number) => Promise.resolve(a + 1)) + ._((b) => b * 3); + const res = p.$(2); + assert.equal(res instanceof Promise, true); + assert.equal(await res, 9); + }); +}); diff --git a/packages/pipeline/src/pipeline.lib.ts b/packages/pipeline/src/pipeline.lib.ts new file mode 100644 index 0000000..178b341 --- /dev/null +++ b/packages/pipeline/src/pipeline.lib.ts @@ -0,0 +1,36 @@ +export type PipelineFn = (input: I) => O; + +export class Pipeline { + private readonly fns: Array<(value: any) => any>; + + public constructor(fns: Array<(value: any) => any>) { + this.fns = fns.slice(); + } + + public _(fn: PipelineFn): Pipeline { + return new Pipeline([...this.fns, fn]); + } + + public apply(value: I): O { + let result: any = value; + for (const fn of this.fns) { + if (result instanceof Promise) { + result = result.then(fn); + } else { + const r = fn(result); + result = r; + } + } + return result as O; + } + + public $(value: I): O { + return this.apply(value); + } +} + +export const pipeline = { + _(fn: PipelineFn): Pipeline { + return new Pipeline([fn]); + }, +}; diff --git a/packages/pipeline/tsconfig.json b/packages/pipeline/tsconfig.json new file mode 100644 index 0000000..79fb341 --- /dev/null +++ b/packages/pipeline/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": {} +} diff --git a/packages/pipeline/tsup.config.ts b/packages/pipeline/tsup.config.ts new file mode 100644 index 0000000..98f9d66 --- /dev/null +++ b/packages/pipeline/tsup.config.ts @@ -0,0 +1 @@ +export { default } from "../../tsup.config.base.ts";