diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0dbfb3e59..c697f6942 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -180,6 +180,10 @@ export default defineConfig({ text: "Either", link: "/fr/v1/api/either/", }, + { + text: "Flow", + link: "/fr/v1/api/flow/", + }, { text: "Generator", link: "/fr/v1/api/generator/", @@ -302,6 +306,10 @@ export default defineConfig({ text: "Either", link: "/en/v1/api/either/", }, + { + text: "Flow", + link: "/en/v1/api/flow/", + }, { text: "Generator", link: "/en/v1/api/generator/", diff --git a/docs/.vitepress/theme/components/monacoTSEditor/useComponent.ts b/docs/.vitepress/theme/components/monacoTSEditor/useComponent.ts index 6daccffd9..1310980b8 100644 --- a/docs/.vitepress/theme/components/monacoTSEditor/useComponent.ts +++ b/docs/.vitepress/theme/components/monacoTSEditor/useComponent.ts @@ -56,6 +56,8 @@ function initMonaco() { "@duplojs/utils/dataParser": ["node_modules/@duplojs/utils/dataParser/index.d.ts"], "@duplojs/utils/dataParserCoerce": ["node_modules/@duplojs/utils/dataParser/parsers/coerce/index.d.ts"], "@duplojs/utils/dataParserExtended": ["node_modules/@duplojs/utils/dataParser/extended/index.d.ts"], + "@duplojs/utils/flow": ["node_modules/@duplojs/utils/flow/index.d.ts"], + "@duplojs/utils/flow/initializer": ["node_modules/@duplojs/utils/flow/initializer.d.ts"], "@duplojs/utils/generator": ["node_modules/@duplojs/utils/generator/index.d.ts"], "@duplojs/utils/number": ["node_modules/@duplojs/utils/number/index.d.ts"], "@duplojs/utils/object": ["node_modules/@duplojs/utils/object/index.d.ts"], diff --git a/docs/en/v1/api/either/index.md b/docs/en/v1/api/either/index.md index 7a81909a4..8b6ddad78 100644 --- a/docs/en/v1/api/either/index.md +++ b/docs/en/v1/api/either/index.md @@ -5,8 +5,8 @@ prev: text: 'Date' link: '/en/v1/api/date/' next: - text: 'Generator' - link: '/en/v1/api/generator/' + text: 'Flow' + link: '/en/v1/api/flow/' --- # Either diff --git a/docs/en/v1/api/flow/breakIf.md b/docs/en/v1/api/flow/breakIf.md new file mode 100644 index 000000000..bd5b47efc --- /dev/null +++ b/docs/en/v1/api/flow/breakIf.md @@ -0,0 +1,47 @@ +--- +outline: [2, 3] +description: "Stops the current flow branch when a predicate matches." +prev: + text: "exec" + link: "/en/v1/api/flow/exec" +next: + text: "exitIf" + link: "/en/v1/api/flow/exitIf" +--- + +# breakIf + +The **`breakIf()`** function tests a value with a predicate and breaks the current flow branch when the predicate returns `true`. If the predicate fails, the value is returned and the flow continues. + +## Interactive example + + + +## Syntax + +```typescript +function breakIf< + GenericValue extends unknown +>( + value: GenericValue, + thePredicate: (value: GenericValue) => boolean +): Generator, GenericValue> +``` + +## Parameters + +- `value`: The value to test. +- `thePredicate`: Predicate used to decide whether the current branch must stop. + +## Return value + +A generator that yields a break effect when the predicate returns `true`, otherwise returns the original value. + +## See also + +- [`exitIf`](/en/v1/api/flow/exitIf) - Exits the whole running flow instead of only breaking locally +- [`run`](/en/v1/api/flow/run) - Executes a flow and handles break effects diff --git a/docs/en/v1/api/flow/create.md b/docs/en/v1/api/flow/create.md new file mode 100644 index 000000000..6d24fcfc2 --- /dev/null +++ b/docs/en/v1/api/flow/create.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Creates a reusable flow object from a flow function." +prev: + text: "Flow" + link: "/en/v1/api/flow/" +next: + text: "run" + link: "/en/v1/api/flow/run" +--- + +# create + +The **`create()`** function wraps a generator-based flow function into a reusable flow object that can later be executed with `F.run()` or composed with `F.exec()`. + +## Interactive example + + + +## Syntax + +```typescript +function create< + GenericTheFlowFunction extends TheFlowFunction +>( + theFunction: GenericTheFlowFunction +): TheFlow +``` + +## Parameters + +- `theFunction`: The generator or async generator function that defines the flow. + +## Return value + +A flow object that stores the provided function and can be executed many times with `F.run()` or `F.exec()`. + +## See also + +- [`run`](/en/v1/api/flow/run) - Executes a created flow +- [`exec`](/en/v1/api/flow/exec) - Executes a created flow inside another flow diff --git a/docs/en/v1/api/flow/createDependence.md b/docs/en/v1/api/flow/createDependence.md new file mode 100644 index 000000000..806bf3081 --- /dev/null +++ b/docs/en/v1/api/flow/createDependence.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Creates a typed dependency handler for the flow system." +prev: + text: "finalizer" + link: "/en/v1/api/flow/finalizer" +next: + text: "inject" + link: "/en/v1/api/flow/inject" +--- + +# createDependence + +The **`createDependence()`** function creates a typed dependency handler identified by a string name. This handler is then used with `F.inject()` and matched against the `dependencies` object passed to `F.run()` or `F.exec()`. + +## Interactive example + + + +## Syntax + +```typescript +function createDependence< + GenericName extends string +>( + name: GenericName +): DependenceHandlerDefinition +``` + +## Parameters + +- `name`: Dependency key used to match a value inside the runner dependency bag. + +## Return value + +A typed dependence handler definition. Once specialized with a type, it can be used with `F.inject()` to retrieve the matching dependency inside a flow. + +## See also + +- [`inject`](/en/v1/api/flow/inject) - Requests a dependency from the runner +- [`run`](/en/v1/api/flow/run) - Provides dependencies to the flow diff --git a/docs/en/v1/api/flow/createInitializer.md b/docs/en/v1/api/flow/createInitializer.md new file mode 100644 index 000000000..f040dd2b6 --- /dev/null +++ b/docs/en/v1/api/flow/createInitializer.md @@ -0,0 +1,52 @@ +--- +outline: [2, 3] +description: "Creates an initializer that returns a value and automatically registers flow cleanup effects." +prev: + text: "step" + link: "/en/v1/api/flow/step" +next: + text: "Flow" + link: "/en/v1/api/flow/" +--- + +# createInitializer + +The **`createInitializer()`** function wraps an initializer and turns it into a flow-compatible generator that can automatically register a `defer` callback, a `finalizer` callback, or both. + +## Interactive example + + + +## Syntax + +```typescript +function createInitializer< + GenericArgs extends unknown[], + GenericOutput extends unknown +>( + initializer: (...args: GenericArgs) => GenericOutput, + params: { + defer?: (output: Awaited) => unknown; + finalizer?: (output: Awaited) => unknown; + } +): (...args: GenericArgs) => Generator | AsyncGenerator +``` + +## Parameters + +- `initializer`: Function producing the value to expose inside the flow. +- `params.defer`: Optional cleanup callback created from the produced value. +- `params.finalizer`: Optional final callback created from the produced value. + +## Return value + +A function returning a generator compatible with `F.run()`. The generator returns the initializer result and registers the configured cleanup effects. + +## See also + +- [`defer`](/en/v1/api/flow/defer) - Registers a cleanup callback +- [`finalizer`](/en/v1/api/flow/finalizer) - Registers a final callback diff --git a/docs/en/v1/api/flow/defer.md b/docs/en/v1/api/flow/defer.md new file mode 100644 index 000000000..ab23457d9 --- /dev/null +++ b/docs/en/v1/api/flow/defer.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Registers a cleanup callback executed when the flow finishes." +prev: + text: "exitIf" + link: "/en/v1/api/flow/exitIf" +next: + text: "finalizer" + link: "/en/v1/api/flow/finalizer" +--- + +# defer + +The **`defer()`** function registers a cleanup callback that the flow runtime executes after the flow ends. It is useful to release resources or run side effects after a return or a break. + +## Interactive example + + + +## Syntax + +```typescript +function defer< + GenericOutput extends unknown +>( + theFunction: () => GenericOutput +): Generator, undefined> +``` + +## Parameters + +- `theFunction`: Cleanup callback to run when the flow finishes. + +## Return value + +A generator yielding a defer effect. The callback result itself is not returned by the flow. + +## See also + +- [`finalizer`](/en/v1/api/flow/finalizer) - Registers another end-of-flow callback +- [`run`](/en/v1/api/flow/run) - Executes deferred callbacks when the flow completes diff --git a/docs/en/v1/api/flow/exec.md b/docs/en/v1/api/flow/exec.md new file mode 100644 index 000000000..ada919fff --- /dev/null +++ b/docs/en/v1/api/flow/exec.md @@ -0,0 +1,52 @@ +--- +outline: [2, 3] +description: "Executes a nested flow inside the current flow." +prev: + text: "run" + link: "/en/v1/api/flow/run" +next: + text: "breakIf" + link: "/en/v1/api/flow/breakIf" +--- + +# exec + +The **`exec()`** function runs a nested flow from inside the current flow. It lets you compose flows while forwarding steps, exits, finalizers, and dependency injection to the outer runner. + +## Interactive example + + + +## Syntax + +```typescript +function exec< + GenericFlow extends TheFlowFunction | TheFlow | TheFlowGenerator +>( + theFlow: GenericFlow, + params?: { + input?: unknown; + dependencies?: Record; + } +): Generator | AsyncGenerator +``` + +## Parameters + +- `theFlow`: A flow function, a created flow, or an existing generator to execute. +- `params.input`: Optional input passed to the nested flow. +- `params.dependencies`: Optional dependency overrides for the nested execution. + +## Return value + +A generator compatible with the current flow. When the nested flow breaks, `exec()` returns the break value locally. Other supported effects continue to propagate outward. + +## See also + +- [`run`](/en/v1/api/flow/run) - Executes the root flow +- [`create`](/en/v1/api/flow/create) - Creates a reusable flow +- [`exitIf`](/en/v1/api/flow/exitIf) - Exits a flow from any nested depth diff --git a/docs/en/v1/api/flow/exitIf.md b/docs/en/v1/api/flow/exitIf.md new file mode 100644 index 000000000..1803e7c73 --- /dev/null +++ b/docs/en/v1/api/flow/exitIf.md @@ -0,0 +1,47 @@ +--- +outline: [2, 3] +description: "Exits the running flow when a predicate matches, even from nested flows." +prev: + text: "breakIf" + link: "/en/v1/api/flow/breakIf" +next: + text: "defer" + link: "/en/v1/api/flow/defer" +--- + +# exitIf + +The **`exitIf()`** function tests a value with a predicate and exits the running flow when the predicate returns `true`. Because exit effects are forwarded through `F.exec()`, it can stop a flow from deep nested levels. + +## Interactive example + + + +## Syntax + +```typescript +function exitIf< + GenericValue extends unknown +>( + value: GenericValue, + thePredicate: (value: GenericValue) => boolean +): Generator, GenericValue> +``` + +## Parameters + +- `value`: The value to test. +- `thePredicate`: Predicate used to decide whether the running flow must exit. + +## Return value + +A generator that yields an exit effect when the predicate returns `true`, otherwise returns the original value. + +## See also + +- [`breakIf`](/en/v1/api/flow/breakIf) - Stops only the current local branch +- [`exec`](/en/v1/api/flow/exec) - Forwards exit effects across nested flows diff --git a/docs/en/v1/api/flow/finalizer.md b/docs/en/v1/api/flow/finalizer.md new file mode 100644 index 000000000..e4c147490 --- /dev/null +++ b/docs/en/v1/api/flow/finalizer.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Registers a final callback handled by the flow runner." +prev: + text: "defer" + link: "/en/v1/api/flow/defer" +next: + text: "createDependence" + link: "/en/v1/api/flow/createDependence" +--- + +# finalizer + +The **`finalizer()`** function registers a final callback collected by the flow runner. It is designed for end-of-flow logic that should stay inside the flow effect system. + +## Interactive example + + + +## Syntax + +```typescript +function finalizer< + GenericOutput extends unknown +>( + theFunction: () => GenericOutput +): Generator, undefined> +``` + +## Parameters + +- `theFunction`: Callback collected by the runner and executed when the flow finishes. + +## Return value + +A generator yielding a finalizer effect. The callback result is handled by the runner, not by the flow body. + +## See also + +- [`defer`](/en/v1/api/flow/defer) - Registers a cleanup callback +- [`run`](/en/v1/api/flow/run) - Collects and executes finalizers diff --git a/docs/en/v1/api/flow/index.md b/docs/en/v1/api/flow/index.md new file mode 100644 index 000000000..8c24a8388 --- /dev/null +++ b/docs/en/v1/api/flow/index.md @@ -0,0 +1,68 @@ +--- +outline: [2, 3] +description: "Generator-based control-flow helpers to compose synchronous and asynchronous workflows with typed effects, steps, exits, breaks, and dependency injection." +prev: + text: 'Either' + link: '/en/v1/api/either/' +next: + text: 'Generator' + link: '/en/v1/api/generator/' +--- + +# Flow + +Generator-based control-flow helpers to compose synchronous and asynchronous workflows with typed effects, steps, exits, breaks, and dependency injection. + +## How to import? + +The library exposes the `DFlow` and `F` namespaces from the main entry **or** via direct import (tree-shaking friendly), which lets you only load what you need. + +```typescript +import { DFlow, F } from "@duplojs/utils"; +import * as DFlow from "@duplojs/utils/flow"; +import * as F from "@duplojs/utils/flow"; +import { createInitializer } from "@duplojs/utils/flow/initializer"; +``` + +`createInitializer` is documented with the flow helpers, but it is imported from `@duplojs/utils/flow/initializer`. + +## Flow creation and execution + +### [create](/en/v1/api/flow/create) +Creates a reusable flow object from a flow function. + +### [run](/en/v1/api/flow/run) +Executes a flow and returns its final value. + +### [exec](/en/v1/api/flow/exec) +Executes a nested flow inside the current flow. + +## Control flow + +### [breakIf](/en/v1/api/flow/breakIf) +Stops the current flow branch when a predicate matches. + +### [exitIf](/en/v1/api/flow/exitIf) +Exits the running flow when a predicate matches, even from nested flows. + +### [step](/en/v1/api/flow/step) +Registers a named step and can optionally compute a value. + +## Lifecycle and cleanup + +### [defer](/en/v1/api/flow/defer) +Registers a cleanup callback executed when the flow finishes. + +### [finalizer](/en/v1/api/flow/finalizer) +Registers a final callback handled by the flow runner. + +### [createInitializer](/en/v1/api/flow/createInitializer) +Creates an initializer that returns a value and automatically registers flow cleanup effects. + +## Dependencies + +### [createDependence](/en/v1/api/flow/createDependence) +Creates a typed dependency handler for the flow system. + +### [inject](/en/v1/api/flow/inject) +Requests a dependency from the flow runner. diff --git a/docs/en/v1/api/flow/inject.md b/docs/en/v1/api/flow/inject.md new file mode 100644 index 000000000..d39dcaa84 --- /dev/null +++ b/docs/en/v1/api/flow/inject.md @@ -0,0 +1,48 @@ +--- +outline: [2, 3] +description: "Requests a dependency from the flow runner." +prev: + text: "createDependence" + link: "/en/v1/api/flow/createDependence" +next: + text: "step" + link: "/en/v1/api/flow/step" +--- + +# inject + +The **`inject()`** function declares that a flow needs a dependency. The actual value is provided by `F.run()` or `F.exec()` through the `dependencies` parameter. + +## Interactive example + + + +## Syntax + +```typescript +function inject< + GenericDependenceHandler extends DependenceHandler +>( + dependenceHandler: GenericDependenceHandler +): Generator< + Injection, + ReturnType +> +``` + +## Parameters + +- `dependenceHandler`: Dependency descriptor created with `F.createDependence()`. + +## Return value + +A generator yielding an injection effect. Once the runner injects the matching dependency, the generator returns the injected value. + +## See also + +- [`run`](/en/v1/api/flow/run) - Provides dependencies to the flow +- [`exec`](/en/v1/api/flow/exec) - Can override dependencies for nested flows diff --git a/docs/en/v1/api/flow/run.md b/docs/en/v1/api/flow/run.md new file mode 100644 index 000000000..fde9aa9c6 --- /dev/null +++ b/docs/en/v1/api/flow/run.md @@ -0,0 +1,54 @@ +--- +outline: [2, 3] +description: "Executes a flow and returns its final value." +prev: + text: "create" + link: "/en/v1/api/flow/create" +next: + text: "exec" + link: "/en/v1/api/flow/exec" +--- + +# run + +The **`run()`** function is the entry point of the flow system. It executes a synchronous or asynchronous flow, handles effects such as breaks, exits, finalizers, steps, and injections, then returns the final value. + +## Interactive example + + + +## Syntax + +```typescript +function run< + GenericFlow extends TheFlowFunction | TheFlow +>( + theFlow: GenericFlow, + params?: { + input?: unknown; + includeDetails?: boolean; + dependencies?: Record; + } +): unknown +``` + +## Parameters + +- `theFlow`: The flow function or created flow to execute. +- `params.input`: Optional input passed to the flow. +- `params.includeDetails`: When `true`, returns an object with the final result and collected step names. +- `params.dependencies`: Dependency bag used to satisfy `F.inject()` requests. + +## Return value + +The final flow result, or a `Promise` when the executed flow is asynchronous. When `includeDetails` is enabled, the return value becomes `{ result, steps }`. + +## See also + +- [`create`](/en/v1/api/flow/create) - Creates a reusable flow +- [`exec`](/en/v1/api/flow/exec) - Executes a nested flow from inside another flow +- [`inject`](/en/v1/api/flow/inject) - Requests a dependency from the runner diff --git a/docs/en/v1/api/flow/step.md b/docs/en/v1/api/flow/step.md new file mode 100644 index 000000000..943f8b9dd --- /dev/null +++ b/docs/en/v1/api/flow/step.md @@ -0,0 +1,48 @@ +--- +outline: [2, 3] +description: "Registers a named step and can optionally compute a value." +prev: + text: "inject" + link: "/en/v1/api/flow/inject" +next: + text: "createInitializer" + link: "/en/v1/api/flow/createInitializer" +--- + +# step + +The **`step()`** function records a named step in a flow. When `includeDetails` is enabled in `F.run()`, the collected step names are returned alongside the final result. + +## Interactive example + + + +## Syntax + +```typescript +function step< + GenericName extends string, + GenericOutput extends unknown = void +>( + name: GenericName, + theFunction?: () => GenericOutput +): Generator | AsyncGenerator +``` + +## Parameters + +- `name`: Step label stored in the execution details. +- `theFunction`: Optional callback executed after the step is emitted. Its result becomes the return value of `step()`. + +## Return value + +A generator yielding a step effect. It returns `undefined` when no callback is provided, otherwise returns the callback result. + +## See also + +- [`run`](/en/v1/api/flow/run) - Collects step names when `includeDetails` is enabled +- [`exec`](/en/v1/api/flow/exec) - Forwards step effects from nested flows diff --git a/docs/en/v1/api/generator/index.md b/docs/en/v1/api/generator/index.md index e27460df1..0a5897c93 100644 --- a/docs/en/v1/api/generator/index.md +++ b/docs/en/v1/api/generator/index.md @@ -2,8 +2,8 @@ outline: [2, 3] description: "Functions to manipulate JavaScript generators in a functional and type-safe way. Generators allow you to process data sequences lazily, computing values only when they are needed." prev: - text: 'Either' - link: '/en/v1/api/either/' + text: 'Flow' + link: '/en/v1/api/flow/' next: text: 'String' link: '/en/v1/api/string/' diff --git a/docs/en/v1/api/index.md b/docs/en/v1/api/index.md index 89fb9dd2b..1cbb39115 100644 --- a/docs/en/v1/api/index.md +++ b/docs/en/v1/api/index.md @@ -26,6 +26,9 @@ Immutable date/time API with `TheDate` (date object), `TheTime` (duration), and ## [🔀 Either](/en/v1/api/either/) Either monad for functional error handling. Avoid exceptions and manage success/error results explicitly and type-safely. +## [🌊 Flow](/en/v1/api/flow/) +Generator-based control-flow helpers to compose workflows with steps, cleanup, and dependency injection. + ## [⚡ Generator](/en/v1/api/generator/) Utility functions to work with generators and create lazy sequences. Ideal for efficiently handling large amounts of data. diff --git a/docs/examples/v1/api/flow/breakIf/tryout.doc.ts b/docs/examples/v1/api/flow/breakIf/tryout.doc.ts new file mode 100644 index 000000000..e3e69aac5 --- /dev/null +++ b/docs/examples/v1/api/flow/breakIf/tryout.doc.ts @@ -0,0 +1,9 @@ +import { F } from "@duplojs/utils"; + +const result = F.run( + function *() { + yield *F.breakIf(2, (value) => value === 2); + return "done"; + }, +); +// result: 2 diff --git a/docs/examples/v1/api/flow/create/tryout.doc.ts b/docs/examples/v1/api/flow/create/tryout.doc.ts new file mode 100644 index 000000000..3691e03bd --- /dev/null +++ b/docs/examples/v1/api/flow/create/tryout.doc.ts @@ -0,0 +1,10 @@ +import { F } from "@duplojs/utils"; + +const greetingFlow = F.create( + function *(name: string) { + return `hello ${name}`; + }, +); + +const result = F.run(greetingFlow, { input: "Ada" }); +// result: "hello Ada" diff --git a/docs/examples/v1/api/flow/createDependence/tryout.doc.ts b/docs/examples/v1/api/flow/createDependence/tryout.doc.ts new file mode 100644 index 000000000..26d97c98a --- /dev/null +++ b/docs/examples/v1/api/flow/createDependence/tryout.doc.ts @@ -0,0 +1,12 @@ +import { F } from "@duplojs/utils"; + +const database = F.createDependence("database"); + +const result = F.run( + function *() { + const connection = yield *F.inject(database); + return connection; + }, + { dependencies: { database: "main-db" } }, +); +// result: "main-db" diff --git a/docs/examples/v1/api/flow/createInitializer/tryout.doc.ts b/docs/examples/v1/api/flow/createInitializer/tryout.doc.ts new file mode 100644 index 000000000..55d7606c2 --- /dev/null +++ b/docs/examples/v1/api/flow/createInitializer/tryout.doc.ts @@ -0,0 +1,13 @@ +import { F } from "@duplojs/utils"; + +const openSession = F.createInitializer( + (name: string) => ({ name }), + { finalizer: (session) => session.name }, +); + +const result = F.run( + function *() { + return yield *openSession("Ada"); + }, +); +// result: { name: "Ada" } diff --git a/docs/examples/v1/api/flow/defer/tryout.doc.ts b/docs/examples/v1/api/flow/defer/tryout.doc.ts new file mode 100644 index 000000000..f3696cf22 --- /dev/null +++ b/docs/examples/v1/api/flow/defer/tryout.doc.ts @@ -0,0 +1,12 @@ +import { F } from "@duplojs/utils"; + +const logs: string[] = []; + +const result = F.run( + function *() { + yield *F.defer(() => logs.push("close")); + return "done"; + }, +); +// result: "done" +// logs: ["close"] diff --git a/docs/examples/v1/api/flow/exec/tryout.doc.ts b/docs/examples/v1/api/flow/exec/tryout.doc.ts new file mode 100644 index 000000000..adedcaeda --- /dev/null +++ b/docs/examples/v1/api/flow/exec/tryout.doc.ts @@ -0,0 +1,14 @@ +import { F } from "@duplojs/utils"; + +const upperCaseFlow = F.create( + function *(input: string) { + return input.toUpperCase(); + }, +); + +const result = F.run( + function *() { + return yield *F.exec(upperCaseFlow, { input: "hello" }); + }, +); +// result: "HELLO" diff --git a/docs/examples/v1/api/flow/exitIf/tryout.doc.ts b/docs/examples/v1/api/flow/exitIf/tryout.doc.ts new file mode 100644 index 000000000..6b5800a1e --- /dev/null +++ b/docs/examples/v1/api/flow/exitIf/tryout.doc.ts @@ -0,0 +1,15 @@ +import { F } from "@duplojs/utils"; + +const childFlow = F.create( + function *() { + yield *F.exitIf("stop", (value) => value === "stop"); + return "done"; + }, +); + +const result = F.run( + function *() { + return yield *F.exec(childFlow); + }, +); +// result: "stop" diff --git a/docs/examples/v1/api/flow/finalizer/tryout.doc.ts b/docs/examples/v1/api/flow/finalizer/tryout.doc.ts new file mode 100644 index 000000000..c4d85a814 --- /dev/null +++ b/docs/examples/v1/api/flow/finalizer/tryout.doc.ts @@ -0,0 +1,12 @@ +import { F } from "@duplojs/utils"; + +const logs: string[] = []; + +const result = F.run( + function *() { + yield *F.finalizer(() => logs.push("flush")); + return "done"; + }, +); +// result: "done" +// logs: ["flush"] diff --git a/docs/examples/v1/api/flow/inject/tryout.doc.ts b/docs/examples/v1/api/flow/inject/tryout.doc.ts new file mode 100644 index 000000000..26d97c98a --- /dev/null +++ b/docs/examples/v1/api/flow/inject/tryout.doc.ts @@ -0,0 +1,12 @@ +import { F } from "@duplojs/utils"; + +const database = F.createDependence("database"); + +const result = F.run( + function *() { + const connection = yield *F.inject(database); + return connection; + }, + { dependencies: { database: "main-db" } }, +); +// result: "main-db" diff --git a/docs/examples/v1/api/flow/run/tryout.doc.ts b/docs/examples/v1/api/flow/run/tryout.doc.ts new file mode 100644 index 000000000..fec802674 --- /dev/null +++ b/docs/examples/v1/api/flow/run/tryout.doc.ts @@ -0,0 +1,11 @@ +import { F } from "@duplojs/utils"; + +const result = F.run( + function *() { + yield *F.step("load user"); + yield *F.breakIf(2, (value) => value === 2); + return "done"; + }, + { includeDetails: true }, +); +// result: { result: 2, steps: ["load user"] } diff --git a/docs/examples/v1/api/flow/step/tryout.doc.ts b/docs/examples/v1/api/flow/step/tryout.doc.ts new file mode 100644 index 000000000..fd79ead9c --- /dev/null +++ b/docs/examples/v1/api/flow/step/tryout.doc.ts @@ -0,0 +1,10 @@ +import { F } from "@duplojs/utils"; + +const result = F.run( + function *() { + yield *F.step("load config"); + return "done"; + }, + { includeDetails: true }, +); +// result: { result: "done", steps: ["load config"] } diff --git a/docs/fr/v1/api/either/index.md b/docs/fr/v1/api/either/index.md index ff8f630ac..f0882a050 100644 --- a/docs/fr/v1/api/either/index.md +++ b/docs/fr/v1/api/either/index.md @@ -5,8 +5,8 @@ prev: text: 'Date' link: '/fr/v1/api/date/' next: - text: 'Generator' - link: '/fr/v1/api/generator/' + text: 'Flow' + link: '/fr/v1/api/flow/' --- # Either diff --git a/docs/fr/v1/api/flow/breakIf.md b/docs/fr/v1/api/flow/breakIf.md new file mode 100644 index 000000000..9589abd68 --- /dev/null +++ b/docs/fr/v1/api/flow/breakIf.md @@ -0,0 +1,47 @@ +--- +outline: [2, 3] +description: "Arrête la branche courante du flow quand un prédicat correspond." +prev: + text: "exec" + link: "/fr/v1/api/flow/exec" +next: + text: "exitIf" + link: "/fr/v1/api/flow/exitIf" +--- + +# breakIf + +La fonction **`breakIf()`** teste une valeur avec un prédicat et arrête la branche courante du flow quand le prédicat retourne `true`. Si le prédicat échoue, la valeur est retournée et le flow continue. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function breakIf< + GenericValue extends unknown +>( + value: GenericValue, + thePredicate: (value: GenericValue) => boolean +): Generator, GenericValue> +``` + +## Paramètres + +- `value` : La valeur à tester. +- `thePredicate` : Prédicat utilisé pour décider si la branche courante doit s'arrêter. + +## Valeur de retour + +Un générateur qui émet un effet de break quand le prédicat retourne `true`, sinon retourne la valeur d'origine. + +## Voir aussi + +- [`exitIf`](/fr/v1/api/flow/exitIf) - Quitte tout le flow en cours au lieu de seulement break localement +- [`run`](/fr/v1/api/flow/run) - Exécute un flow et gère les effets de break diff --git a/docs/fr/v1/api/flow/create.md b/docs/fr/v1/api/flow/create.md new file mode 100644 index 000000000..66b7e0a0c --- /dev/null +++ b/docs/fr/v1/api/flow/create.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Crée un objet flow réutilisable à partir d'une fonction de flow." +prev: + text: "Flow" + link: "/fr/v1/api/flow/" +next: + text: "run" + link: "/fr/v1/api/flow/run" +--- + +# create + +La fonction **`create()`** wrap une fonction de flow basée sur un générateur dans un objet flow réutilisable qui peut ensuite être exécuté avec `F.run()` ou composé avec `F.exec()`. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function create< + GenericTheFlowFunction extends TheFlowFunction +>( + theFunction: GenericTheFlowFunction +): TheFlow +``` + +## Paramètres + +- `theFunction` : La fonction génératrice ou async génératrice qui définit le flow. + +## Valeur de retour + +Un objet flow qui stocke la fonction fournie et peut être exécuté plusieurs fois avec `F.run()` ou `F.exec()`. + +## Voir aussi + +- [`run`](/fr/v1/api/flow/run) - Exécute un flow créé +- [`exec`](/fr/v1/api/flow/exec) - Exécute un flow créé dans un autre flow diff --git a/docs/fr/v1/api/flow/createDependence.md b/docs/fr/v1/api/flow/createDependence.md new file mode 100644 index 000000000..1fb1ad1c7 --- /dev/null +++ b/docs/fr/v1/api/flow/createDependence.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Crée un descripteur de dépendance typé pour le système de flow." +prev: + text: "finalizer" + link: "/fr/v1/api/flow/finalizer" +next: + text: "inject" + link: "/fr/v1/api/flow/inject" +--- + +# createDependence + +La fonction **`createDependence()`** crée un descripteur de dépendance typé identifié par un nom de chaîne. Ce handler est ensuite utilisé avec `F.inject()` et associé à l'objet `dependencies` transmis à `F.run()` ou `F.exec()`. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function createDependence< + GenericName extends string +>( + name: GenericName +): DependenceHandlerDefinition +``` + +## Paramètres + +- `name` : Clé de dépendance utilisée pour associer une valeur dans le sac de dépendances du runner. + +## Valeur de retour + +Une définition de handler de dépendance typée. Une fois spécialisée avec un type, elle peut être utilisée avec `F.inject()` pour récupérer la dépendance correspondante dans un flow. + +## Voir aussi + +- [`inject`](/fr/v1/api/flow/inject) - Demande une dépendance au runner +- [`run`](/fr/v1/api/flow/run) - Fournit les dépendances au flow diff --git a/docs/fr/v1/api/flow/createInitializer.md b/docs/fr/v1/api/flow/createInitializer.md new file mode 100644 index 000000000..d28b2cdd3 --- /dev/null +++ b/docs/fr/v1/api/flow/createInitializer.md @@ -0,0 +1,52 @@ +--- +outline: [2, 3] +description: "Crée un initializer qui retourne une valeur et enregistre automatiquement des effets de nettoyage de flow." +prev: + text: "step" + link: "/fr/v1/api/flow/step" +next: + text: "Flow" + link: "/fr/v1/api/flow/" +--- + +# createInitializer + +La fonction **`createInitializer()`** wrap un initializer et le transforme en générateur compatible avec les flows, capable d'enregistrer automatiquement un callback `defer`, un callback `finalizer`, ou les deux. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function createInitializer< + GenericArgs extends unknown[], + GenericOutput extends unknown +>( + initializer: (...args: GenericArgs) => GenericOutput, + params: { + defer?: (output: Awaited) => unknown; + finalizer?: (output: Awaited) => unknown; + } +): (...args: GenericArgs) => Generator | AsyncGenerator +``` + +## Paramètres + +- `initializer` : Fonction qui produit la valeur à exposer dans le flow. +- `params.defer` : Callback de nettoyage optionnel construit à partir de la valeur produite. +- `params.finalizer` : Callback final optionnel construit à partir de la valeur produite. + +## Valeur de retour + +Une fonction qui retourne un générateur compatible avec `F.run()`. Le générateur retourne le résultat de l'initializer et enregistre les effets de nettoyage configurés. + +## Voir aussi + +- [`defer`](/fr/v1/api/flow/defer) - Enregistre un callback de nettoyage +- [`finalizer`](/fr/v1/api/flow/finalizer) - Enregistre un callback final diff --git a/docs/fr/v1/api/flow/defer.md b/docs/fr/v1/api/flow/defer.md new file mode 100644 index 000000000..ea97eb470 --- /dev/null +++ b/docs/fr/v1/api/flow/defer.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Enregistre un callback de nettoyage exécuté quand le flow se termine." +prev: + text: "exitIf" + link: "/fr/v1/api/flow/exitIf" +next: + text: "finalizer" + link: "/fr/v1/api/flow/finalizer" +--- + +# defer + +La fonction **`defer()`** enregistre un callback de nettoyage que le runtime de flow exécute après la fin du flow. Elle est utile pour libérer des ressources ou lancer des effets de bord après un retour ou un break. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function defer< + GenericOutput extends unknown +>( + theFunction: () => GenericOutput +): Generator, undefined> +``` + +## Paramètres + +- `theFunction` : Callback de nettoyage à exécuter quand le flow se termine. + +## Valeur de retour + +Un générateur qui émet un effet de defer. Le résultat du callback n'est pas lui-même retourné par le flow. + +## Voir aussi + +- [`finalizer`](/fr/v1/api/flow/finalizer) - Enregistre un autre callback de fin de flow +- [`run`](/fr/v1/api/flow/run) - Exécute les callbacks différés à la fin du flow diff --git a/docs/fr/v1/api/flow/exec.md b/docs/fr/v1/api/flow/exec.md new file mode 100644 index 000000000..69683b34c --- /dev/null +++ b/docs/fr/v1/api/flow/exec.md @@ -0,0 +1,52 @@ +--- +outline: [2, 3] +description: "Exécute un flow imbriqué dans le flow courant." +prev: + text: "run" + link: "/fr/v1/api/flow/run" +next: + text: "breakIf" + link: "/fr/v1/api/flow/breakIf" +--- + +# exec + +La fonction **`exec()`** exécute un flow imbriqué depuis le flow courant. Elle permet de composer plusieurs flows tout en propageant les steps, exits, finalizers et injections de dépendances vers le runner externe. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function exec< + GenericFlow extends TheFlowFunction | TheFlow | TheFlowGenerator +>( + theFlow: GenericFlow, + params?: { + input?: unknown; + dependencies?: Record; + } +): Generator | AsyncGenerator +``` + +## Paramètres + +- `theFlow` : Une fonction de flow, un flow créé, ou un générateur existant à exécuter. +- `params.input` : Input optionnel transmis au flow imbriqué. +- `params.dependencies` : Overrides optionnels de dépendances pour l'exécution imbriquée. + +## Valeur de retour + +Un générateur compatible avec le flow courant. Quand le flow imbriqué fait un break, `exec()` retourne localement la valeur du break. Les autres effets supportés continuent à remonter vers l'extérieur. + +## Voir aussi + +- [`run`](/fr/v1/api/flow/run) - Exécute le flow racine +- [`create`](/fr/v1/api/flow/create) - Crée un flow réutilisable +- [`exitIf`](/fr/v1/api/flow/exitIf) - Quitte un flow depuis n'importe quelle profondeur diff --git a/docs/fr/v1/api/flow/exitIf.md b/docs/fr/v1/api/flow/exitIf.md new file mode 100644 index 000000000..b99b48809 --- /dev/null +++ b/docs/fr/v1/api/flow/exitIf.md @@ -0,0 +1,47 @@ +--- +outline: [2, 3] +description: "Quitte le flow en cours quand un prédicat correspond, même depuis des flows imbriqués." +prev: + text: "breakIf" + link: "/fr/v1/api/flow/breakIf" +next: + text: "defer" + link: "/fr/v1/api/flow/defer" +--- + +# exitIf + +La fonction **`exitIf()`** teste une valeur avec un prédicat et quitte le flow en cours quand le prédicat retourne `true`. Comme les effets d'exit sont propagés à travers `F.exec()`, elle peut arrêter un flow depuis des niveaux profondément imbriqués. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function exitIf< + GenericValue extends unknown +>( + value: GenericValue, + thePredicate: (value: GenericValue) => boolean +): Generator, GenericValue> +``` + +## Paramètres + +- `value` : La valeur à tester. +- `thePredicate` : Prédicat utilisé pour décider si le flow en cours doit se terminer. + +## Valeur de retour + +Un générateur qui émet un effet d'exit quand le prédicat retourne `true`, sinon retourne la valeur d'origine. + +## Voir aussi + +- [`breakIf`](/fr/v1/api/flow/breakIf) - Arrête seulement la branche locale courante +- [`exec`](/fr/v1/api/flow/exec) - Propage les effets d'exit à travers les flows imbriqués diff --git a/docs/fr/v1/api/flow/finalizer.md b/docs/fr/v1/api/flow/finalizer.md new file mode 100644 index 000000000..90a0c6bef --- /dev/null +++ b/docs/fr/v1/api/flow/finalizer.md @@ -0,0 +1,45 @@ +--- +outline: [2, 3] +description: "Enregistre un callback final géré par le runner de flow." +prev: + text: "defer" + link: "/fr/v1/api/flow/defer" +next: + text: "createDependence" + link: "/fr/v1/api/flow/createDependence" +--- + +# finalizer + +La fonction **`finalizer()`** enregistre un callback final collecté par le runner de flow. Elle est pensée pour de la logique de fin de flow qui doit rester dans le système d'effets du flow. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function finalizer< + GenericOutput extends unknown +>( + theFunction: () => GenericOutput +): Generator, undefined> +``` + +## Paramètres + +- `theFunction` : Callback collecté par le runner et exécuté quand le flow se termine. + +## Valeur de retour + +Un générateur qui émet un effet de finalizer. Le résultat du callback est géré par le runner, pas par le corps du flow. + +## Voir aussi + +- [`defer`](/fr/v1/api/flow/defer) - Enregistre un callback de nettoyage +- [`run`](/fr/v1/api/flow/run) - Collecte et exécute les finalizers diff --git a/docs/fr/v1/api/flow/index.md b/docs/fr/v1/api/flow/index.md new file mode 100644 index 000000000..9df4306d1 --- /dev/null +++ b/docs/fr/v1/api/flow/index.md @@ -0,0 +1,68 @@ +--- +outline: [2, 3] +description: "Helpers de contrôle de flux basés sur des générateurs pour composer des workflows synchrones et asynchrones avec effets typés, étapes, sorties, breaks et injection de dépendances." +prev: + text: 'Either' + link: '/fr/v1/api/either/' +next: + text: 'Generator' + link: '/fr/v1/api/generator/' +--- + +# Flow + +Helpers de contrôle de flux basés sur des générateurs pour composer des workflows synchrones et asynchrones avec effets typés, étapes, sorties, breaks et injection de dépendances. + +## Comment faire les imports ? + +La bibliothèque expose les namespaces `DFlow` et `F` depuis l'entrée principale **ou** en import direct (tree-shaking friendly), ce qui permet de ne charger que ce dont vous avez besoin. + +```typescript +import { DFlow, F } from "@duplojs/utils"; +import * as DFlow from "@duplojs/utils/flow"; +import * as F from "@duplojs/utils/flow"; +import { createInitializer } from "@duplojs/utils/flow/initializer"; +``` + +`createInitializer` est documentée avec les helpers `flow`, mais s'importe depuis `@duplojs/utils/flow/initializer`. + +## Création et exécution de flow + +### [create](/fr/v1/api/flow/create) +Créer un flow réutilisable à partir d'une fonction de flow. + +### [run](/fr/v1/api/flow/run) +Exécute un flow et retourne sa valeur finale. + +### [exec](/fr/v1/api/flow/exec) +Exécute un flow imbriqué dans le flow courant. + +## Contrôle de flux + +### [breakIf](/fr/v1/api/flow/breakIf) +Arrête la branche courante du flow quand un prédicat correspond. + +### [exitIf](/fr/v1/api/flow/exitIf) +Quitte le flow en cours quand un prédicat correspond, même depuis un flow imbriqué. + +### [step](/fr/v1/api/flow/step) +Enregistre une étape nommée et peut optionnellement calculer une valeur. + +## Cycle de vie et nettoyage + +### [defer](/fr/v1/api/flow/defer) +Enregistre un callback de nettoyage exécuté quand le flow se termine. + +### [finalizer](/fr/v1/api/flow/finalizer) +Enregistre un callback final géré par le runner de flow. + +### [createInitializer](/fr/v1/api/flow/createInitializer) +Crée un initializer qui retourne une valeur et enregistre automatiquement des effets de nettoyage. + +## Dépendances + +### [createDependence](/fr/v1/api/flow/createDependence) +Crée un descripteur de dépendance typé pour le système de flow. + +### [inject](/fr/v1/api/flow/inject) +Demande une dépendance au runner de flow. diff --git a/docs/fr/v1/api/flow/inject.md b/docs/fr/v1/api/flow/inject.md new file mode 100644 index 000000000..06fdebfc7 --- /dev/null +++ b/docs/fr/v1/api/flow/inject.md @@ -0,0 +1,48 @@ +--- +outline: [2, 3] +description: "Demande une dépendance au runner de flow." +prev: + text: "createDependence" + link: "/fr/v1/api/flow/createDependence" +next: + text: "step" + link: "/fr/v1/api/flow/step" +--- + +# inject + +La fonction **`inject()`** déclare qu'un flow a besoin d'une dépendance. La valeur réelle est fournie par `F.run()` ou `F.exec()` via le paramètre `dependencies`. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function inject< + GenericDependenceHandler extends DependenceHandler +>( + dependenceHandler: GenericDependenceHandler +): Generator< + Injection, + ReturnType +> +``` + +## Paramètres + +- `dependenceHandler` : Descripteur de dépendance créé avec `F.createDependence()`. + +## Valeur de retour + +Un générateur qui émet un effet d'injection. Une fois que le runner injecte la dépendance correspondante, le générateur retourne la valeur injectée. + +## Voir aussi + +- [`run`](/fr/v1/api/flow/run) - Fournit les dépendances au flow +- [`exec`](/fr/v1/api/flow/exec) - Peut override les dépendances pour des flows imbriqués diff --git a/docs/fr/v1/api/flow/run.md b/docs/fr/v1/api/flow/run.md new file mode 100644 index 000000000..abf59608b --- /dev/null +++ b/docs/fr/v1/api/flow/run.md @@ -0,0 +1,54 @@ +--- +outline: [2, 3] +description: "Exécute un flow et retourne sa valeur finale." +prev: + text: "create" + link: "/fr/v1/api/flow/create" +next: + text: "exec" + link: "/fr/v1/api/flow/exec" +--- + +# run + +La fonction **`run()`** est le point d'entrée du système de flow. Elle exécute un flow synchrone ou asynchrone, gère les effets comme les breaks, exits, finalizers, steps et injections, puis retourne la valeur finale. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function run< + GenericFlow extends TheFlowFunction | TheFlow +>( + theFlow: GenericFlow, + params?: { + input?: unknown; + includeDetails?: boolean; + dependencies?: Record; + } +): unknown +``` + +## Paramètres + +- `theFlow` : La fonction de flow ou le flow créé à exécuter. +- `params.input` : Input optionnel transmis au flow. +- `params.includeDetails` : Quand `true`, retourne un objet avec le résultat final et les noms d'étapes collectés. +- `params.dependencies` : Sac de dépendances utilisé pour satisfaire les demandes `F.inject()`. + +## Valeur de retour + +Le résultat final du flow, ou une `Promise` quand le flow exécuté est asynchrone. Quand `includeDetails` est activé, la valeur de retour devient `{ result, steps }`. + +## Voir aussi + +- [`create`](/fr/v1/api/flow/create) - Crée un flow réutilisable +- [`exec`](/fr/v1/api/flow/exec) - Exécute un flow imbriqué depuis un autre flow +- [`inject`](/fr/v1/api/flow/inject) - Demande une dépendance au runner diff --git a/docs/fr/v1/api/flow/step.md b/docs/fr/v1/api/flow/step.md new file mode 100644 index 000000000..157b4a030 --- /dev/null +++ b/docs/fr/v1/api/flow/step.md @@ -0,0 +1,48 @@ +--- +outline: [2, 3] +description: "Enregistre une étape nommée et peut optionnellement calculer une valeur." +prev: + text: "inject" + link: "/fr/v1/api/flow/inject" +next: + text: "createInitializer" + link: "/fr/v1/api/flow/createInitializer" +--- + +# step + +La fonction **`step()`** enregistre une étape nommée dans un flow. Quand `includeDetails` est activé dans `F.run()`, les noms d'étapes collectés sont retournés avec le résultat final. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function step< + GenericName extends string, + GenericOutput extends unknown = void +>( + name: GenericName, + theFunction?: () => GenericOutput +): Generator | AsyncGenerator +``` + +## Paramètres + +- `name` : Label d'étape stocké dans les détails d'exécution. +- `theFunction` : Callback optionnel exécuté après l'émission du step. Son résultat devient la valeur de retour de `step()`. + +## Valeur de retour + +Un générateur qui émet un effet de step. Il retourne `undefined` quand aucun callback n'est fourni, sinon retourne le résultat du callback. + +## Voir aussi + +- [`run`](/fr/v1/api/flow/run) - Collecte les noms d'étapes quand `includeDetails` est activé +- [`exec`](/fr/v1/api/flow/exec) - Propage les effets de step depuis des flows imbriqués diff --git a/docs/fr/v1/api/generator/index.md b/docs/fr/v1/api/generator/index.md index f627719df..e23e31fb7 100644 --- a/docs/fr/v1/api/generator/index.md +++ b/docs/fr/v1/api/generator/index.md @@ -2,8 +2,8 @@ outline: [2, 3] description: "Fonctions pour manipuler les générateurs JavaScript de manière fonctionnelle et type-safe. Les générateurs permettent de traiter des séquences de données de façon lazy (paresseuse), en ne calculant les valeurs que lorsqu'elles sont nécessaires." prev: - text: 'Either' - link: '/fr/v1/api/either/' + text: 'Flow' + link: '/fr/v1/api/flow/' next: text: 'String' link: '/fr/v1/api/string/' diff --git a/docs/fr/v1/api/index.md b/docs/fr/v1/api/index.md index 1a151b287..5a8980ba4 100644 --- a/docs/fr/v1/api/index.md +++ b/docs/fr/v1/api/index.md @@ -26,6 +26,9 @@ API date/time immutable avec `TheDate` (objet date), `TheTime` (durée) et leurs ## [🔀 Either](/fr/v1/api/either/) Monade Either pour la gestion d'erreurs fonctionnelle. Évitez les exceptions et gérez les résultats success/error de manière explicite et type-safe. +## [🌊 Flow](/fr/v1/api/flow/) +Helpers de contrôle de flux basés sur des générateurs pour composer des workflows avec étapes, nettoyage et injection de dépendances. + ## [⚡ Generator](/fr/v1/api/generator/) Fonctions utilitaires pour travailler avec les générateurs et créer des séquences lazy. Idéal pour gérer de grandes quantités de données efficacement. diff --git a/docs/public/libs/v1/clean/flag.d.ts b/docs/public/libs/v1/clean/flag.d.ts index 80fb7fb00..09cfaebeb 100644 --- a/docs/public/libs/v1/clean/flag.d.ts +++ b/docs/public/libs/v1/clean/flag.d.ts @@ -31,7 +31,7 @@ export interface FlagHandler>(entity: GenericInputEntity): GetKindValue[GenericName]; - has(entity: GenericInputEntity): Extract>; + has(entity: GenericInputEntity): entity is Extract>; } export interface Flag extends Kind> { } diff --git a/docs/public/libs/v1/clean/primitive/operations/equal.d.ts b/docs/public/libs/v1/clean/primitive/operations/equal.d.ts index 0efa7425c..aa741d98f 100644 --- a/docs/public/libs/v1/clean/primitive/operations/equal.d.ts +++ b/docs/public/libs/v1/clean/primitive/operations/equal.d.ts @@ -1,4 +1,4 @@ -import { type ToLargeEnsemble, type Unwrap } from "../../../common"; +import { type Unwrap } from "../../../common"; import { type Primitive, type Primitives } from "../base"; /** * Compares two wrapped primitives (or a primitive and a raw value) with a type guard. @@ -34,5 +34,5 @@ import { type Primitive, type Primitives } from "../base"; * @namespace C * */ -export declare function equal>> | ToLargeEnsemble>)>(value: GenericValue): (input: GenericInput) => input is GenericInput & Primitive>; -export declare function equal>> | ToLargeEnsemble>)>(input: GenericInput, value: GenericValue): input is GenericInput & Primitive>; +export declare function equal> | Unwrap)>(value: GenericValue): (input: GenericInput) => input is GenericInput & Primitive>; +export declare function equal> | Unwrap)>(input: GenericInput, value: GenericValue): input is GenericInput & Primitive>; diff --git a/docs/public/libs/v1/common/kind.cjs b/docs/public/libs/v1/common/kind.cjs index d62e62ecf..625d48fbc 100644 --- a/docs/public/libs/v1/common/kind.cjs +++ b/docs/public/libs/v1/common/kind.cjs @@ -24,7 +24,8 @@ function createKind(name) { }, has(input) { return input - && typeof input === "object" + && (typeof input === "object" + || typeof input === "function") && runTimeKey in input; }, getValue(input) { diff --git a/docs/public/libs/v1/common/kind.d.ts b/docs/public/libs/v1/common/kind.d.ts index 568f66fa9..85fcc1812 100644 --- a/docs/public/libs/v1/common/kind.d.ts +++ b/docs/public/libs/v1/common/kind.d.ts @@ -94,6 +94,7 @@ export interface ReservedKindNamespace { DuplojsUtilsError: true; DuplojsUtilsClean: true; DuplojsUtilsDate: true; + DuplojsUtilsFlow: true; } type ForbiddenKindNamespace = (ForbiddenKindCharacters & ForbiddenString>); /** diff --git a/docs/public/libs/v1/common/kind.mjs b/docs/public/libs/v1/common/kind.mjs index 3021c0ad8..046656e71 100644 --- a/docs/public/libs/v1/common/kind.mjs +++ b/docs/public/libs/v1/common/kind.mjs @@ -22,7 +22,8 @@ function createKind(name) { }, has(input) { return input - && typeof input === "object" + && (typeof input === "object" + || typeof input === "function") && runTimeKey in input; }, getValue(input) { diff --git a/docs/public/libs/v1/flow/breakIf.cjs b/docs/public/libs/v1/flow/breakIf.cjs new file mode 100644 index 000000000..4dd6bb5be --- /dev/null +++ b/docs/public/libs/v1/flow/breakIf.cjs @@ -0,0 +1,14 @@ +'use strict'; + +var _break = require('./theFlow/break.cjs'); + +function* breakIf(value, thePredicate) { + if (thePredicate(value) === true) { + yield _break.createBreak(value); + } + else { + return value; + } +} + +exports.breakIf = breakIf; diff --git a/docs/public/libs/v1/flow/breakIf.d.ts b/docs/public/libs/v1/flow/breakIf.d.ts new file mode 100644 index 000000000..660bfc795 --- /dev/null +++ b/docs/public/libs/v1/flow/breakIf.d.ts @@ -0,0 +1,50 @@ +import { type NeverCoalescing } from "../common"; +import { type Break } from "./theFlow"; +/** + * Breaks a flow when a predicate matches a value. + * + * **Supported call styles:** + * - Classic boolean predicate: `breakIf(value, thePredicate)` -> yields a break effect or returns the value + * - Classic predicate overload: `breakIf(value, thePredicate)` -> narrows the matched value type before breaking + * + * `breakIf` is designed to be used inside `F.run(...)` or a nested flow executed by `F.exec(...)`. + * When the predicate returns `true`, the current flow stops with the provided value as a break result. + * When the predicate returns `false`, the original value is returned and the flow continues normally. + * + * ```ts + * F.run( + * function *() { + * yield *F.breakIf(2, (value) => value === 2); + * + * return "test"; + * }, + * ); // 2 + * + * F.run( + * function *() { + * const value = yield *F.breakIf("keep", (value) => value === "stop"); + * return value; + * }, + * ); // "keep" + * + * F.run( + * function *() { + * yield *F.step("before break"); + * yield *F.breakIf(2, (value) => value === 2); + * return "done"; + * }, + * { includeDetails: true }, + * ); // { result: 2, steps: ["before break"] } + * ``` + * + * @remarks + * - Use `breakIf` to stop the current flow without exiting outer flows + * + * @see [`F.exitIf`](https://utils.duplojs.dev/en/v1/api/flow/exitIf) To exit a flow instead of breaking it + * @see https://utils.duplojs.dev/en/v1/api/flow/breakIf + * + * @namespace F + * + */ +export declare function breakIf(value: GenericValue, thePredicate: (value: GenericValue) => value is GenericPredicate): Generator, GenericPredicate>>, Exclude>; +export declare function breakIf(value: GenericValue, thePredicate: (value: GenericValue) => boolean): Generator, GenericValue>; diff --git a/docs/public/libs/v1/flow/breakIf.mjs b/docs/public/libs/v1/flow/breakIf.mjs new file mode 100644 index 000000000..c6dfdeea6 --- /dev/null +++ b/docs/public/libs/v1/flow/breakIf.mjs @@ -0,0 +1,12 @@ +import { createBreak } from './theFlow/break.mjs'; + +function* breakIf(value, thePredicate) { + if (thePredicate(value) === true) { + yield createBreak(value); + } + else { + return value; + } +} + +export { breakIf }; diff --git a/docs/public/libs/v1/flow/defer.cjs b/docs/public/libs/v1/flow/defer.cjs new file mode 100644 index 000000000..bf569f018 --- /dev/null +++ b/docs/public/libs/v1/flow/defer.cjs @@ -0,0 +1,12 @@ +'use strict'; + +var defer$1 = require('./theFlow/defer.cjs'); + +/** + * {@include flow/defer/index.md} + */ +function* defer(theFunction) { + yield defer$1.createDefer(theFunction); +} + +exports.defer = defer; diff --git a/docs/public/libs/v1/flow/defer.d.ts b/docs/public/libs/v1/flow/defer.d.ts new file mode 100644 index 000000000..3dd1f0468 --- /dev/null +++ b/docs/public/libs/v1/flow/defer.d.ts @@ -0,0 +1,47 @@ +import { type Defer } from "./theFlow"; +/** + * Registers a cleanup callback that runs when the flow finishes. + * + * **Supported call styles:** + * - Classic: `defer(theFunction)` -> yields a defer effect + * + * `defer` stores a callback that is executed after the flow has completed. + * It is useful for releasing resources, closing handles, or running cleanup logic after a `break` or a normal return. + * Use it inside `F.run(...)` or inside subflows executed by `F.exec(...)`. + * + * ```ts + * F.run( + * function *() { + * yield *F.defer(() => void console.log("close connection")); + * return "done"; + * }, + * ); // "done" + * + * F.run( + * function *() { + * yield *F.defer(() => void console.log("clear cache")); + * yield *F.breakIf(2, (value) => value === 2); + * return "done"; + * }, + * ); // 2 + * + * await F.run( + * async function *() { + * yield *F.defer(async() => { + * await Promise.resolve(); + * }); + * return Promise.resolve("done"); + * }, + * ); // Promise<"done"> + * ``` + * + * @remarks + * - Deferred callbacks run after the flow result has been computed + * + * @see [`F.finalizer`](https://utils.duplojs.dev/en/v1/api/flow/finalizer) To register final logic in the same flow system + * @see https://utils.duplojs.dev/en/v1/api/flow/defer + * + * @namespace F + * + */ +export declare function defer(theFunction: () => GenericOutput): (Generator, undefined> | (GenericOutput extends Promise ? AsyncGenerator, undefined> : never)); diff --git a/docs/public/libs/v1/flow/defer.mjs b/docs/public/libs/v1/flow/defer.mjs new file mode 100644 index 000000000..e15d48d6d --- /dev/null +++ b/docs/public/libs/v1/flow/defer.mjs @@ -0,0 +1,10 @@ +import { createDefer } from './theFlow/defer.mjs'; + +/** + * {@include flow/defer/index.md} + */ +function* defer(theFunction) { + yield createDefer(theFunction); +} + +export { defer }; diff --git a/docs/public/libs/v1/flow/exec.cjs b/docs/public/libs/v1/flow/exec.cjs new file mode 100644 index 000000000..6a1ca60cc --- /dev/null +++ b/docs/public/libs/v1/flow/exec.cjs @@ -0,0 +1,125 @@ +'use strict'; + +var index = require('./theFlow/index.cjs'); +var defer = require('./theFlow/defer.cjs'); +var finalizer = require('./theFlow/finalizer.cjs'); +var justExec = require('../common/justExec.cjs'); +var _break = require('./theFlow/break.cjs'); +var exit = require('./theFlow/exit.cjs'); +var step = require('./theFlow/step.cjs'); +var injection = require('./theFlow/injection.cjs'); +var dependence = require('./theFlow/dependence.cjs'); +var forward = require('../common/forward.cjs'); + +/** + * {@include flow/exec/index.md} + */ +function exec(theFlow, ...[params]) { + let result = undefined; + let deferFunctions = undefined; + const generator = justExec.justExec(() => { + if (Symbol.asyncIterator in theFlow || Symbol.iterator in theFlow) { + return forward.forward(theFlow); + } + else if (typeof theFlow === "function") { + return theFlow(params?.input); + } + else { + return index.theFLowKind.getValue(theFlow).run(params?.input); + } + }); + if (Symbol.asyncIterator in generator) { + return (async function* () { + try { + do { + result = await generator.next(); + if (result.done === true) { + break; + } + else if (_break.breakKind.has(result.value)) { + result = await generator.return(_break.breakKind.getValue(result.value).value); + break; + } + else if (exit.exitKind.has(result.value)) { + yield result.value; + } + else if (defer.deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(defer.deferKind.getValue(result.value)); + } + else if (finalizer.finalizerKind.has(result.value)) { + yield result.value; + } + else if (step.stepKind.has(result.value)) { + yield result.value; + } + else if (injection.injectionKind.has(result.value)) { + const injectionProperties = injection.injectionKind.getValue(result.value); + const dependenceName = dependence.dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + yield result.value; + } + else { + injectionProperties.inject(params.dependencies[dependenceName]); + } + } + } while (true); + return result.value; + } + finally { + await generator.return(undefined); + if (deferFunctions) { + await Promise.all(deferFunctions.map(justExec.justExec)); + } + } + })(); + } + return (function* () { + try { + do { + result = generator.next(); + if (result.done === true) { + break; + } + else if (_break.breakKind.has(result.value)) { + result = generator.return(_break.breakKind.getValue(result.value).value); + break; + } + else if (exit.exitKind.has(result.value)) { + yield result.value; + } + else if (defer.deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(defer.deferKind.getValue(result.value)); + } + else if (finalizer.finalizerKind.has(result.value)) { + yield result.value; + } + else if (step.stepKind.has(result.value)) { + yield result.value; + } + else if (injection.injectionKind.has(result.value)) { + const injectionProperties = injection.injectionKind.getValue(result.value); + const dependenceName = dependence.dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + yield result.value; + } + else { + injectionProperties.inject(params.dependencies[dependenceName]); + } + } + } while (true); + return result.value; + } + finally { + generator.return(undefined); + if (deferFunctions) { + deferFunctions.map(justExec.justExec); + } + } + })(); +} + +exports.exec = exec; diff --git a/docs/public/libs/v1/flow/exec.d.ts b/docs/public/libs/v1/flow/exec.d.ts new file mode 100644 index 000000000..349d0482d --- /dev/null +++ b/docs/public/libs/v1/flow/exec.d.ts @@ -0,0 +1,83 @@ +import { type SimplifyTopLevel, type IsEqual, type IsExtends, type Or } from "../common"; +import { type TheFlowGenerator, type TheFlow, type TheFlowFunction, type FlowInput, type WrapFlow, type Exit, type Break, type Injection, type Step, type FlowDependencies } from "./theFlow"; +import { type Finalizer } from "./theFlow/finalizer"; +type ComputeExecParams> = SimplifyTopLevel<(Or<[ + IsEqual, + IsEqual, + IsExtends +]> extends true ? { + input?: GenericInput; +} : { + input: GenericInput; +}) & { + dependencies?: GenericDependencies; +}>; +export type ExecResult = GenericFlow extends TheFlow ? InferredFunction extends TheFlowFunction ? InferredGenerator extends TheFlowGenerator ? [ + ((InferredEffect extends Break ? InferredValue : never) | InferredOutput), + Extract +] extends [ + infer InferredOutput, + infer InferredEffect +] ? InferredGenerator extends AsyncGenerator ? AsyncGenerator : Generator : never : never : never : never; +/** + * Executes a nested flow inside the current flow. + * + * **Supported call styles:** + * - Classic with a flow function: `exec(theFlow, params?)` -> runs the provided flow function + * - Classic with a flow instance: `exec(theFlow, params?)` -> runs a flow created with `F.create(...)` + * - Classic with a generator: `exec(theGenerator, params?)` -> runs an existing generator directly + * + * `exec` lets a parent flow call another flow while staying in the same execution model. + * Break values are converted into the local return value of `exec`, while exit, step, injection, and finalizer effects are forwarded to the outer flow. + * It can be used in synchronous and asynchronous flows. + * + * ```ts + * + * const upperCaseFlow = F.create( + * function *(input: string) { + * return input.toUpperCase(); + * }, + * ); + * + * const userFlow = F.create( + * function *(id: number) { + * return `user-${id}`; + * }, + * ); + * + * const breakableFlow = F.create( + * function *(value: number) { + * yield *F.breakIf(value, (current) => current === 2); + * return "done"; + * }, + * ); + * + * F.run( + * function *() { + * return yield *F.exec(upperCaseFlow, { input: "hello" }); + * }, + * ); // "HELLO" + * + * F.run( + * function *() { + * return yield *F.exec(userFlow, { input: 42 }); + * }, + * ); // "user-42" + * + * F.run( + * function *() { + * return yield *F.exec(breakableFlow, { input: 2 }); + * }, + * ``` + * + * @remarks + * - `exec` is useful for composing small flows into larger ones + * + * @see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) To execute the root flow + * @see https://utils.duplojs.dev/en/v1/api/flow/exec + * + * @namespace F + * + */ +export declare function exec, const GenericParams extends ComputeExecParams, FlowDependencies>>(theFlow: GenericFlow, ...[params]: ({} extends GenericParams ? [params?: GenericParams] : [params: GenericParams])): ExecResult>; +export {}; diff --git a/docs/public/libs/v1/flow/exec.mjs b/docs/public/libs/v1/flow/exec.mjs new file mode 100644 index 000000000..ce14d3b6a --- /dev/null +++ b/docs/public/libs/v1/flow/exec.mjs @@ -0,0 +1,123 @@ +import { theFLowKind } from './theFlow/index.mjs'; +import { deferKind } from './theFlow/defer.mjs'; +import { finalizerKind } from './theFlow/finalizer.mjs'; +import { justExec } from '../common/justExec.mjs'; +import { breakKind } from './theFlow/break.mjs'; +import { exitKind } from './theFlow/exit.mjs'; +import { stepKind } from './theFlow/step.mjs'; +import { injectionKind } from './theFlow/injection.mjs'; +import { dependenceHandlerKind } from './theFlow/dependence.mjs'; +import { forward } from '../common/forward.mjs'; + +/** + * {@include flow/exec/index.md} + */ +function exec(theFlow, ...[params]) { + let result = undefined; + let deferFunctions = undefined; + const generator = justExec(() => { + if (Symbol.asyncIterator in theFlow || Symbol.iterator in theFlow) { + return forward(theFlow); + } + else if (typeof theFlow === "function") { + return theFlow(params?.input); + } + else { + return theFLowKind.getValue(theFlow).run(params?.input); + } + }); + if (Symbol.asyncIterator in generator) { + return (async function* () { + try { + do { + result = await generator.next(); + if (result.done === true) { + break; + } + else if (breakKind.has(result.value)) { + result = await generator.return(breakKind.getValue(result.value).value); + break; + } + else if (exitKind.has(result.value)) { + yield result.value; + } + else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(deferKind.getValue(result.value)); + } + else if (finalizerKind.has(result.value)) { + yield result.value; + } + else if (stepKind.has(result.value)) { + yield result.value; + } + else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + yield result.value; + } + else { + injectionProperties.inject(params.dependencies[dependenceName]); + } + } + } while (true); + return result.value; + } + finally { + await generator.return(undefined); + if (deferFunctions) { + await Promise.all(deferFunctions.map(justExec)); + } + } + })(); + } + return (function* () { + try { + do { + result = generator.next(); + if (result.done === true) { + break; + } + else if (breakKind.has(result.value)) { + result = generator.return(breakKind.getValue(result.value).value); + break; + } + else if (exitKind.has(result.value)) { + yield result.value; + } + else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(deferKind.getValue(result.value)); + } + else if (finalizerKind.has(result.value)) { + yield result.value; + } + else if (stepKind.has(result.value)) { + yield result.value; + } + else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + yield result.value; + } + else { + injectionProperties.inject(params.dependencies[dependenceName]); + } + } + } while (true); + return result.value; + } + finally { + generator.return(undefined); + if (deferFunctions) { + deferFunctions.map(justExec); + } + } + })(); +} + +export { exec }; diff --git a/docs/public/libs/v1/flow/exitIf.cjs b/docs/public/libs/v1/flow/exitIf.cjs new file mode 100644 index 000000000..504a0a0a8 --- /dev/null +++ b/docs/public/libs/v1/flow/exitIf.cjs @@ -0,0 +1,14 @@ +'use strict'; + +var exit = require('./theFlow/exit.cjs'); + +function* exitIf(value, thePredicate) { + if (thePredicate(value) === true) { + yield exit.createExit(value); + } + else { + return value; + } +} + +exports.exitIf = exitIf; diff --git a/docs/public/libs/v1/flow/exitIf.d.ts b/docs/public/libs/v1/flow/exitIf.d.ts new file mode 100644 index 000000000..8facc4f34 --- /dev/null +++ b/docs/public/libs/v1/flow/exitIf.d.ts @@ -0,0 +1,67 @@ +import { type NeverCoalescing } from "../common"; +import { type Exit } from "./theFlow"; +/** + * Exits a flow when a predicate matches a value. + * + * **Supported call styles:** + * - Classic boolean predicate: `exitIf(value, thePredicate)` -> yields an exit effect or returns the value + * - Classic predicate overload: `exitIf(value, thePredicate)` -> narrows the matched value type before exiting + * + * `exitIf` is designed to be used inside `F.run(...)` or a nested flow executed by `F.exec(...)`. + * When the predicate returns `true`, the current flow exits with the provided value. + * When the predicate returns `false`, the original value is returned and the flow continues normally. + * Because the exit effect is forwarded through nested `exec(...)` calls, it can stop a deeply nested flow from any depth. + * + * ```ts + * const thirdLevelFlow = F.create( + * function *() { + * yield *F.exitIf("stop", (value) => value === "stop"); + * return "done"; + * }, + * ); + * + * const secondLevelFlow = F.create( + * function *() { + * return yield *F.exec(thirdLevelFlow); + * }, + * ); + * + * const firstLevelFlow = F.create( + * function *() { + * return yield *F.exec(secondLevelFlow); + * }, + * ); + * + * F.run( + * function *() { + * return yield *F.exitIf(2, (value) => value === 2); + * }, + * ); // 2 + * + * F.run( + * function *() { + * const value = yield *F.exitIf("keep", (value) => value === "stop"); + * return value; + * }, + * ); // "keep" + * + * F.run( + * function *() { + * yield *F.step("before deep exit"); + * return yield *F.exec(firstLevelFlow); + * }, + * { includeDetails: true }, + * ); // { result: "stop", steps: ["before deep exit"] } + * ``` + * + * @remarks + * - Use `exitIf` when the whole running flow should stop with a value + * + * @see [`F.breakIf`](https://utils.duplojs.dev/en/v1/api/flow/breakIf) To stop only the current local flow branch + * @see https://utils.duplojs.dev/en/v1/api/flow/exitIf + * + * @namespace F + * + */ +export declare function exitIf(value: GenericValue, thePredicate: (value: GenericValue) => value is GenericPredicate): Generator, GenericPredicate>>, Exclude>; +export declare function exitIf(value: GenericValue, thePredicate: (value: GenericValue) => boolean): Generator, GenericValue>; diff --git a/docs/public/libs/v1/flow/exitIf.mjs b/docs/public/libs/v1/flow/exitIf.mjs new file mode 100644 index 000000000..8ee8fe0fd --- /dev/null +++ b/docs/public/libs/v1/flow/exitIf.mjs @@ -0,0 +1,12 @@ +import { createExit } from './theFlow/exit.mjs'; + +function* exitIf(value, thePredicate) { + if (thePredicate(value) === true) { + yield createExit(value); + } + else { + return value; + } +} + +export { exitIf }; diff --git a/docs/public/libs/v1/flow/finalizer.cjs b/docs/public/libs/v1/flow/finalizer.cjs new file mode 100644 index 000000000..30918e4e0 --- /dev/null +++ b/docs/public/libs/v1/flow/finalizer.cjs @@ -0,0 +1,12 @@ +'use strict'; + +var finalizer$1 = require('./theFlow/finalizer.cjs'); + +/** + * {@include flow/finalizer/index.md} + */ +function* finalizer(theFunction) { + yield finalizer$1.createFinalizer(theFunction); +} + +exports.finalizer = finalizer; diff --git a/docs/public/libs/v1/flow/finalizer.d.ts b/docs/public/libs/v1/flow/finalizer.d.ts new file mode 100644 index 000000000..696dba059 --- /dev/null +++ b/docs/public/libs/v1/flow/finalizer.d.ts @@ -0,0 +1,47 @@ +import { type Finalizer } from "./theFlow"; +/** + * Registers a final callback handled by the flow runner. + * + * **Supported call styles:** + * - Classic: `finalizer(theFunction)` -> yields a finalizer effect + * + * `finalizer` registers logic that is executed by the flow runner when the flow completes. + * It is useful for cleanup or post-processing that should stay inside the flow effect system. + * Use it inside `F.run(...)` or inside subflows executed by `F.exec(...)`. + * + * ```ts + * F.run( + * function *() { + * yield *F.finalizer(() => void console.log("close connection")); + * return "done"; + * }, + * ); // "done" + * + * F.run( + * function *() { + * yield *F.finalizer(() => void console.log("clear cache")); + * yield *F.breakIf(2, (value) => value === 2); + * return "done"; + * }, + * ); // 2 + * + * await F.run( + * async function *() { + * yield *F.finalizer(async() => { + * await Promise.resolve(); + * }); + * return Promise.resolve("done"); + * }, + * ); // Promise<"done"> + * ``` + * + * @remarks + * - Finalizers are collected by the flow runner and executed after the flow ends + * + * @see [`F.defer`](https://utils.duplojs.dev/en/v1/api/flow/defer) For another cleanup-oriented effect + * @see https://utils.duplojs.dev/en/v1/api/flow/finalizer + * + * @namespace F + * + */ +export declare function finalizer(theFunction: () => GenericOutput): (Generator, undefined> | (GenericOutput extends Promise ? AsyncGenerator, undefined> : never)); diff --git a/docs/public/libs/v1/flow/finalizer.mjs b/docs/public/libs/v1/flow/finalizer.mjs new file mode 100644 index 000000000..901546c8c --- /dev/null +++ b/docs/public/libs/v1/flow/finalizer.mjs @@ -0,0 +1,10 @@ +import { createFinalizer } from './theFlow/finalizer.mjs'; + +/** + * {@include flow/finalizer/index.md} + */ +function* finalizer(theFunction) { + yield createFinalizer(theFunction); +} + +export { finalizer }; diff --git a/docs/public/libs/v1/flow/index.cjs b/docs/public/libs/v1/flow/index.cjs new file mode 100644 index 000000000..01f454ef6 --- /dev/null +++ b/docs/public/libs/v1/flow/index.cjs @@ -0,0 +1,50 @@ +'use strict'; + +var run = require('./run.cjs'); +var index = require('./theFlow/index.cjs'); +var breakIf = require('./breakIf.cjs'); +var defer$1 = require('./defer.cjs'); +var exec = require('./exec.cjs'); +var exitIf = require('./exitIf.cjs'); +var finalizer$1 = require('./finalizer.cjs'); +var inject = require('./inject.cjs'); +var step$1 = require('./step.cjs'); +var initializer = require('./initializer.cjs'); +var kind = require('./kind.cjs'); +var step = require('./theFlow/step.cjs'); +var exit = require('./theFlow/exit.cjs'); +var _break = require('./theFlow/break.cjs'); +var injection = require('./theFlow/injection.cjs'); +var defer = require('./theFlow/defer.cjs'); +var finalizer = require('./theFlow/finalizer.cjs'); +var dependence = require('./theFlow/dependence.cjs'); + + + +exports.MissingDependenceError = run.MissingDependenceError; +exports.run = run.run; +exports.create = index.create; +exports.theFLowKind = index.theFLowKind; +exports.breakIf = breakIf.breakIf; +exports.defer = defer$1.defer; +exports.exec = exec.exec; +exports.exitIf = exitIf.exitIf; +exports.finalizer = finalizer$1.finalizer; +exports.inject = inject.inject; +exports.step = step$1.step; +exports.createInitializer = initializer.createInitializer; +exports.createFlowKind = kind.createFlowKind; +exports.createStep = step.createStep; +exports.stepKind = step.stepKind; +exports.createExit = exit.createExit; +exports.exitKind = exit.exitKind; +exports.breakKind = _break.breakKind; +exports.createBreak = _break.createBreak; +exports.createInjection = injection.createInjection; +exports.injectionKind = injection.injectionKind; +exports.createDefer = defer.createDefer; +exports.deferKind = defer.deferKind; +exports.createFinalizer = finalizer.createFinalizer; +exports.finalizerKind = finalizer.finalizerKind; +exports.createDependence = dependence.createDependence; +exports.dependenceHandlerKind = dependence.dependenceHandlerKind; diff --git a/docs/public/libs/v1/flow/index.d.ts b/docs/public/libs/v1/flow/index.d.ts new file mode 100644 index 000000000..66a6f76cf --- /dev/null +++ b/docs/public/libs/v1/flow/index.d.ts @@ -0,0 +1,12 @@ +export * from "./types"; +export * from "./run"; +export * from "./theFlow"; +export * from "./breakIf"; +export * from "./defer"; +export * from "./exec"; +export * from "./exitIf"; +export * from "./finalizer"; +export * from "./inject"; +export * from "./step"; +export * from "./initializer"; +export * from "./kind"; diff --git a/docs/public/libs/v1/flow/index.mjs b/docs/public/libs/v1/flow/index.mjs new file mode 100644 index 000000000..854988d93 --- /dev/null +++ b/docs/public/libs/v1/flow/index.mjs @@ -0,0 +1,18 @@ +export { MissingDependenceError, run } from './run.mjs'; +export { create, theFLowKind } from './theFlow/index.mjs'; +export { breakIf } from './breakIf.mjs'; +export { defer } from './defer.mjs'; +export { exec } from './exec.mjs'; +export { exitIf } from './exitIf.mjs'; +export { finalizer } from './finalizer.mjs'; +export { inject } from './inject.mjs'; +export { step } from './step.mjs'; +export { createInitializer } from './initializer.mjs'; +export { createFlowKind } from './kind.mjs'; +export { createStep, stepKind } from './theFlow/step.mjs'; +export { createExit, exitKind } from './theFlow/exit.mjs'; +export { breakKind, createBreak } from './theFlow/break.mjs'; +export { createInjection, injectionKind } from './theFlow/injection.mjs'; +export { createDefer, deferKind } from './theFlow/defer.mjs'; +export { createFinalizer, finalizerKind } from './theFlow/finalizer.mjs'; +export { createDependence, dependenceHandlerKind } from './theFlow/dependence.mjs'; diff --git a/docs/public/libs/v1/flow/initializer.cjs b/docs/public/libs/v1/flow/initializer.cjs new file mode 100644 index 000000000..359e21a7b --- /dev/null +++ b/docs/public/libs/v1/flow/initializer.cjs @@ -0,0 +1,38 @@ +'use strict'; + +var defer = require('./theFlow/defer.cjs'); +var finalizer = require('./theFlow/finalizer.cjs'); + +/** + * {@include flow/createInitializer/index.md} + */ +function createInitializer(initializer, params) { + return (...args) => { + const result = initializer(...args); + const defer$1 = params.defer; + const finalizer$1 = params.finalizer; + if (result instanceof Promise) { + return (async function* () { + const awaitedResult = await result; + if (defer$1) { + yield defer.createDefer(() => defer$1(awaitedResult)); + } + if (finalizer$1) { + yield finalizer.createFinalizer(() => finalizer$1(awaitedResult)); + } + return awaitedResult; + })(); + } + return (function* () { + if (defer$1) { + yield defer.createDefer(() => defer$1(result)); + } + if (finalizer$1) { + yield finalizer.createFinalizer(() => finalizer$1(result)); + } + return result; + })(); + }; +} + +exports.createInitializer = createInitializer; diff --git a/docs/public/libs/v1/flow/initializer.d.ts b/docs/public/libs/v1/flow/initializer.d.ts new file mode 100644 index 000000000..ff1d3a75e --- /dev/null +++ b/docs/public/libs/v1/flow/initializer.d.ts @@ -0,0 +1,75 @@ +import { type AnyFunction, type IsExtends, type Or } from "../common"; +import { type RequireAtLeastOne } from "../object"; +import { type Defer, type Finalizer } from "./theFlow"; +export interface CreateInitializerParams { + defer?(output: Awaited): unknown; + finalizer?(output: Awaited): unknown; +} +export type Initializer> = Extract<(...args: GenericArgs) => (((GenericParams["finalizer"] extends AnyFunction ? Finalizer> : never) | (GenericParams["defer"] extends AnyFunction ? Defer> : never)) extends infer InferredEffect ? (Generator> | (Or<[ + IsExtends>, + IsExtends>>, + IsExtends>> +]> extends true ? AsyncGenerator> : never)) : never), any>; +/** + * Creates an initializer that returns a value and automatically registers flow cleanup effects. + * + * **Supported call styles:** + * - Classic: `createInitializer(initializer, params)` -> returns a function that can be yielded inside a flow + * + * `createInitializer` wraps an initializer function and turns its result into a flow-friendly generator. + * Depending on the provided options, it can register a `defer` callback, a `finalizer` callback, or both, using the produced value. + * The returned initializer can then be executed inside `F.run(...)` like any other flow generator. + * + * ```ts + * const userInitializer = createInitializer( + * (name: string) => ({ name }), + * { defer: (user) => void console.log(`close:${user.name}`) }, + * ); + * + * F.run( + * function *() { + * return yield *userInitializer("Ada"); + * }, + * ); // { name: "Ada" } + * + * const finalizerLogs: string[] = []; + * const tokenInitializer = createInitializer( + * (id: number) => `token-${id}`, + * { finalizer: (token) => finalizerLogs.push(token) }, + * ); + * + * F.run( + * function *() { + * return yield *tokenInitializer(42); + * }, + * ); // "token-42" + * + * const asyncInitializer = createInitializer( + * (name: string) => Promise.resolve({ + * name, + * ready: true, + * }), + * { defer: (user) => void console.log(`async:${user.name}`) }, + * ); + * + * void await F.run( + * async function *() { + * const value = yield *asyncInitializer("Linus"); + * // Promise<{ name: string; ready: true }> + * + * return; + * }, + * ); + * ``` + * + * @remarks + * - `createInitializer` is useful when a setup step should also declare matching cleanup logic + * + * @see [`F.defer`](https://utils.duplojs.dev/en/v1/api/flow/defer) For cleanup callbacks + * @see [`F.finalizer`](https://utils.duplojs.dev/en/v1/api/flow/finalizer) For final callbacks + * @see https://utils.duplojs.dev/en/v1/api/flow/createInitializer + * + * @namespace F + * + */ +export declare function createInitializer>(initializer: (...args: GenericArgs) => GenericOutput, params: GenericParams & RequireAtLeastOne): Initializer; diff --git a/docs/public/libs/v1/flow/initializer.mjs b/docs/public/libs/v1/flow/initializer.mjs new file mode 100644 index 000000000..8a56e4890 --- /dev/null +++ b/docs/public/libs/v1/flow/initializer.mjs @@ -0,0 +1,36 @@ +import { createDefer } from './theFlow/defer.mjs'; +import { createFinalizer } from './theFlow/finalizer.mjs'; + +/** + * {@include flow/createInitializer/index.md} + */ +function createInitializer(initializer, params) { + return (...args) => { + const result = initializer(...args); + const defer = params.defer; + const finalizer = params.finalizer; + if (result instanceof Promise) { + return (async function* () { + const awaitedResult = await result; + if (defer) { + yield createDefer(() => defer(awaitedResult)); + } + if (finalizer) { + yield createFinalizer(() => finalizer(awaitedResult)); + } + return awaitedResult; + })(); + } + return (function* () { + if (defer) { + yield createDefer(() => defer(result)); + } + if (finalizer) { + yield createFinalizer(() => finalizer(result)); + } + return result; + })(); + }; +} + +export { createInitializer }; diff --git a/docs/public/libs/v1/flow/inject.cjs b/docs/public/libs/v1/flow/inject.cjs new file mode 100644 index 000000000..73cad50c2 --- /dev/null +++ b/docs/public/libs/v1/flow/inject.cjs @@ -0,0 +1,19 @@ +'use strict'; + +var injection = require('./theFlow/injection.cjs'); + +/** + * {@include flow/inject/index.md} + */ +function* inject(dependenceHandler) { + let dependence = undefined; + yield injection.createInjection({ + dependenceHandler, + inject: (value) => { + dependence = value; + }, + }); + return dependence; +} + +exports.inject = inject; diff --git a/docs/public/libs/v1/flow/inject.d.ts b/docs/public/libs/v1/flow/inject.d.ts new file mode 100644 index 000000000..8da016a1f --- /dev/null +++ b/docs/public/libs/v1/flow/inject.d.ts @@ -0,0 +1,46 @@ +import { type DependenceHandler, type Injection } from "./theFlow"; +/** + * Requests a dependency from the flow runner. + * + * **Supported call styles:** + * - Classic: `inject(dependenceHandler)` -> yields an injection effect and returns the injected value + * + * `inject` lets a flow declare that it needs a dependency by using a dependence handler created with `F.createDependence(...)`. + * When `F.run(...)` or `F.exec(...)` receives matching dependencies, the requested value is injected back into the flow. + * If the dependency is missing, the runner throws a missing dependence error. + * + * ```ts + * const database = F.createDependence("database"); + * + * F.run( + * function *() { + * const connection = yield *F.inject(database); + * return connection; + * }, + * { dependencies: { database: "main-db" } }, + * ); // "main-db" + * + * F.run( + * function *() { + * return yield *F.exec( + * function *() { + * const connection = yield *F.inject(database); + * return connection; + * }, + * { dependencies: { database: "replica-db" } }, + * ); + * }, + * { dependencies: { database: "main-db" } }, + * ); // "replica-db" + * ``` + * + * @remarks + * - `inject` keeps dependencies explicit in flow definitions + * + * @see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) For providing dependencies + * @see https://utils.duplojs.dev/en/v1/api/flow/inject + * + * @namespace F + * + */ +export declare function inject(dependenceHandler: GenericDependenceHandler): Generator, ReturnType>; diff --git a/docs/public/libs/v1/flow/inject.mjs b/docs/public/libs/v1/flow/inject.mjs new file mode 100644 index 000000000..92b47bee0 --- /dev/null +++ b/docs/public/libs/v1/flow/inject.mjs @@ -0,0 +1,17 @@ +import { createInjection } from './theFlow/injection.mjs'; + +/** + * {@include flow/inject/index.md} + */ +function* inject(dependenceHandler) { + let dependence = undefined; + yield createInjection({ + dependenceHandler, + inject: (value) => { + dependence = value; + }, + }); + return dependence; +} + +export { inject }; diff --git a/docs/public/libs/v1/flow/kind.cjs b/docs/public/libs/v1/flow/kind.cjs new file mode 100644 index 000000000..8253d1df6 --- /dev/null +++ b/docs/public/libs/v1/flow/kind.cjs @@ -0,0 +1,9 @@ +'use strict'; + +var kind = require('../common/kind.cjs'); + +const createFlowKind = kind.createKindNamespace( +// @ts-expect-error reserved kind namespace +"DuplojsUtilsFlow"); + +exports.createFlowKind = createFlowKind; diff --git a/docs/public/libs/v1/flow/kind.d.ts b/docs/public/libs/v1/flow/kind.d.ts new file mode 100644 index 000000000..3e8cd4dac --- /dev/null +++ b/docs/public/libs/v1/flow/kind.d.ts @@ -0,0 +1 @@ +export declare const createFlowKind: (name: GenericName & import("../string").ForbiddenString) => import("../common").KindHandler>; diff --git a/docs/public/libs/v1/flow/kind.mjs b/docs/public/libs/v1/flow/kind.mjs new file mode 100644 index 000000000..78b71a360 --- /dev/null +++ b/docs/public/libs/v1/flow/kind.mjs @@ -0,0 +1,7 @@ +import { createKindNamespace } from '../common/kind.mjs'; + +const createFlowKind = createKindNamespace( +// @ts-expect-error reserved kind namespace +"DuplojsUtilsFlow"); + +export { createFlowKind }; diff --git a/docs/public/libs/v1/flow/run.cjs b/docs/public/libs/v1/flow/run.cjs new file mode 100644 index 000000000..086c67527 --- /dev/null +++ b/docs/public/libs/v1/flow/run.cjs @@ -0,0 +1,139 @@ +'use strict'; + +var index = require('./theFlow/index.cjs'); +var defer = require('./theFlow/defer.cjs'); +var finalizer = require('./theFlow/finalizer.cjs'); +var kind$1 = require('./kind.cjs'); +var dependence = require('./theFlow/dependence.cjs'); +var _break = require('./theFlow/break.cjs'); +var exit = require('./theFlow/exit.cjs'); +var step = require('./theFlow/step.cjs'); +var injection = require('./theFlow/injection.cjs'); +var justExec = require('../common/justExec.cjs'); +var kind = require('../common/kind.cjs'); + +class MissingDependenceError extends kind.kindHeritage("missing-dependence-error", kind$1.createFlowKind("missing-dependence-error"), Error) { + dependenceHandler; + constructor(dependenceHandler) { + super({}, [`Missing dependence : ${dependence.dependenceHandlerKind.getValue(dependenceHandler)}`]); + this.dependenceHandler = dependenceHandler; + } +} +/** + * {@include flow/run/index.md} + */ +function run(theFlow, ...[params]) { + let result = undefined; + let deferFunctions = undefined; + let steps = undefined; + const generator = typeof theFlow === "function" + ? theFlow(params?.input) + : index.theFLowKind.getValue(theFlow).run(params?.input); + if (Symbol.asyncIterator in generator) { + return (async function () { + try { + do { + result = await generator.next(); + if (result.done === true) { + break; + } + else if (_break.breakKind.has(result.value)) { + result = await generator.return(_break.breakKind.getValue(result.value).value); + break; + } + else if (exit.exitKind.has(result.value)) { + result = await generator.return(exit.exitKind.getValue(result.value).value); + break; + } + else if (defer.deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(defer.deferKind.getValue(result.value)); + } + else if (finalizer.finalizerKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(finalizer.finalizerKind.getValue(result.value)); + } + else if (params?.includeDetails === true + && step.stepKind.has(result.value)) { + steps ??= []; + steps.push(step.stepKind.getValue(result.value)); + } + else if (injection.injectionKind.has(result.value)) { + const injectionProperties = injection.injectionKind.getValue(result.value); + const dependenceName = dependence.dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + throw new MissingDependenceError(injectionProperties.dependenceHandler); + } + injectionProperties.inject(params.dependencies[dependenceName]); + } + } while (true); + return params?.includeDetails === true + ? { + result: result.value, + steps: steps ?? [], + } + : result.value; + } + finally { + await generator.return(undefined); + if (deferFunctions) { + await Promise.all(deferFunctions.map(justExec.justExec)); + } + } + })(); + } + try { + do { + result = generator.next(); + if (result.done === true) { + break; + } + else if (_break.breakKind.has(result.value)) { + result = generator.return(_break.breakKind.getValue(result.value).value); + break; + } + else if (exit.exitKind.has(result.value)) { + result = generator.return(exit.exitKind.getValue(result.value).value); + break; + } + else if (defer.deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(defer.deferKind.getValue(result.value)); + } + else if (finalizer.finalizerKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(finalizer.finalizerKind.getValue(result.value)); + } + else if (params?.includeDetails === true + && step.stepKind.has(result.value)) { + steps ??= []; + steps.push(step.stepKind.getValue(result.value)); + } + else if (injection.injectionKind.has(result.value)) { + const injectionProperties = injection.injectionKind.getValue(result.value); + const dependenceName = dependence.dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + throw new MissingDependenceError(injectionProperties.dependenceHandler); + } + injectionProperties.inject(params.dependencies[dependenceName]); + } + } while (true); + return (params?.includeDetails === true + ? { + result: result.value, + steps: steps ?? [], + } + : result.value); + } + finally { + generator.return(undefined); + if (deferFunctions) { + deferFunctions.map(justExec.justExec); + } + } +} + +exports.MissingDependenceError = MissingDependenceError; +exports.run = run; diff --git a/docs/public/libs/v1/flow/run.d.ts b/docs/public/libs/v1/flow/run.d.ts new file mode 100644 index 000000000..92caf1042 --- /dev/null +++ b/docs/public/libs/v1/flow/run.d.ts @@ -0,0 +1,80 @@ +import { type SimplifyTopLevel, type IsEqual, type IsExtends, type Or } from "../common"; +import { type TheFlow, type TheFlowFunction, type FlowInput, type WrapFlow, type TheFlowGenerator, type Exit, type Break, type Step, type FlowDependencies, type DependenceHandler, type ExtractFlowGenerator } from "./theFlow"; +type ComputeRunParams> = SimplifyTopLevel<(Or<[ + IsEqual, + IsEqual, + IsExtends +]> extends true ? { + input?: GenericInput; +} : { + input: GenericInput; +}) & { + includeDetails?: boolean; +} & ({} extends GenericDependencies ? { + dependencies?: GenericDependencies; +} : { + dependencies: GenericDependencies; +})>; +export interface FlowDetails { + result: GenericValue; + steps: GenericStepName[]; +} +export type RunResult = (GenericFlow extends TheFlow ? InferredFunction extends TheFlowFunction ? InferredGenerator extends TheFlowGenerator ? ((InferredEffect extends Exit ? InferredValue : InferredEffect extends Break ? InferredValue : never) | InferredOutput) extends infer InferredResult ? IsEqual extends true ? FlowDetails ? InferredName : never> : InferredResult : never : never : never : never) extends infer InferredResult ? ExtractFlowGenerator extends AsyncGenerator ? Promise : InferredResult : never; +declare const MissingDependenceError_base: new (params: { + "@DuplojsUtilsFlow/missing-dependence-error"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("../common").Kind, unknown> & import("../common").Kind, unknown>; +export declare class MissingDependenceError extends MissingDependenceError_base { + dependenceHandler: DependenceHandler; + constructor(dependenceHandler: DependenceHandler); +} +/** + * Runs a flow and resolves its final result. + * + * **Supported call styles:** + * - Classic with a flow function: `run(theFlow, params?)` -> executes the provided flow function + * - Classic with a flow instance: `run(theFlow, params?)` -> executes a flow created with `F.create(...)` + * + * `run` is the entry point of the flow system. + * It executes synchronous or asynchronous flows, handles break and exit effects, collects steps when `includeDetails` is enabled, runs deferred and finalizer callbacks, and injects declared dependencies. + * Use `run` to start a top-level flow and get its final value. + * + * ```ts + * F.run( + * function *(input: string) { + * return input.toUpperCase(); + * }, + * { input: "hello" }, + * ); // "HELLO" + * + * F.run( + * function *() { + * yield *F.step("check cache"); + * yield *F.breakIf(2, (value) => value === 2); + * return "done"; + * }, + * { includeDetails: true }, + * ); // { result: 2, steps: ["check cache"] } + * + * const service = F.createDependence("service"); + * + * F.run( + * function *() { + * const currentService = yield *F.inject(service); + * yield *F.finalizer(() => currentService.toUpperCase()); + * return currentService; + * }, + * { dependencies: { service: "api" } }, + * ); // "api" + * ``` + * + * @remarks + * - `run` returns a `Promise` when the executed flow is asynchronous + * + * @see [`F.exec`](https://utils.duplojs.dev/en/v1/api/flow/exec) To run a nested flow from inside another flow + * @see https://utils.duplojs.dev/en/v1/api/flow/run + * + * @namespace F + * + */ +export declare function run, const GenericParams extends ComputeRunParams, FlowDependencies>>(theFlow: GenericFlow, ...[params]: ({} extends GenericParams ? [params?: GenericParams] : [params: GenericParams])): RunResult>; +export {}; diff --git a/docs/public/libs/v1/flow/run.mjs b/docs/public/libs/v1/flow/run.mjs new file mode 100644 index 000000000..a4f3d6c04 --- /dev/null +++ b/docs/public/libs/v1/flow/run.mjs @@ -0,0 +1,136 @@ +import { theFLowKind } from './theFlow/index.mjs'; +import { deferKind } from './theFlow/defer.mjs'; +import { finalizerKind } from './theFlow/finalizer.mjs'; +import { createFlowKind } from './kind.mjs'; +import { dependenceHandlerKind } from './theFlow/dependence.mjs'; +import { breakKind } from './theFlow/break.mjs'; +import { exitKind } from './theFlow/exit.mjs'; +import { stepKind } from './theFlow/step.mjs'; +import { injectionKind } from './theFlow/injection.mjs'; +import { justExec } from '../common/justExec.mjs'; +import { kindHeritage } from '../common/kind.mjs'; + +class MissingDependenceError extends kindHeritage("missing-dependence-error", createFlowKind("missing-dependence-error"), Error) { + dependenceHandler; + constructor(dependenceHandler) { + super({}, [`Missing dependence : ${dependenceHandlerKind.getValue(dependenceHandler)}`]); + this.dependenceHandler = dependenceHandler; + } +} +/** + * {@include flow/run/index.md} + */ +function run(theFlow, ...[params]) { + let result = undefined; + let deferFunctions = undefined; + let steps = undefined; + const generator = typeof theFlow === "function" + ? theFlow(params?.input) + : theFLowKind.getValue(theFlow).run(params?.input); + if (Symbol.asyncIterator in generator) { + return (async function () { + try { + do { + result = await generator.next(); + if (result.done === true) { + break; + } + else if (breakKind.has(result.value)) { + result = await generator.return(breakKind.getValue(result.value).value); + break; + } + else if (exitKind.has(result.value)) { + result = await generator.return(exitKind.getValue(result.value).value); + break; + } + else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(deferKind.getValue(result.value)); + } + else if (finalizerKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(finalizerKind.getValue(result.value)); + } + else if (params?.includeDetails === true + && stepKind.has(result.value)) { + steps ??= []; + steps.push(stepKind.getValue(result.value)); + } + else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + throw new MissingDependenceError(injectionProperties.dependenceHandler); + } + injectionProperties.inject(params.dependencies[dependenceName]); + } + } while (true); + return params?.includeDetails === true + ? { + result: result.value, + steps: steps ?? [], + } + : result.value; + } + finally { + await generator.return(undefined); + if (deferFunctions) { + await Promise.all(deferFunctions.map(justExec)); + } + } + })(); + } + try { + do { + result = generator.next(); + if (result.done === true) { + break; + } + else if (breakKind.has(result.value)) { + result = generator.return(breakKind.getValue(result.value).value); + break; + } + else if (exitKind.has(result.value)) { + result = generator.return(exitKind.getValue(result.value).value); + break; + } + else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(deferKind.getValue(result.value)); + } + else if (finalizerKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push(finalizerKind.getValue(result.value)); + } + else if (params?.includeDetails === true + && stepKind.has(result.value)) { + steps ??= []; + steps.push(stepKind.getValue(result.value)); + } + else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if (!params?.dependencies + || !(dependenceName in params.dependencies)) { + throw new MissingDependenceError(injectionProperties.dependenceHandler); + } + injectionProperties.inject(params.dependencies[dependenceName]); + } + } while (true); + return (params?.includeDetails === true + ? { + result: result.value, + steps: steps ?? [], + } + : result.value); + } + finally { + generator.return(undefined); + if (deferFunctions) { + deferFunctions.map(justExec); + } + } +} + +export { MissingDependenceError, run }; diff --git a/docs/public/libs/v1/flow/step.cjs b/docs/public/libs/v1/flow/step.cjs new file mode 100644 index 000000000..1e0f4ae2e --- /dev/null +++ b/docs/public/libs/v1/flow/step.cjs @@ -0,0 +1,23 @@ +'use strict'; + +var step$1 = require('./theFlow/step.cjs'); + +/** + * {@include flow/step/index.md} + */ +function step(name, theFunction) { + const result = theFunction?.(); + if (result instanceof Promise) { + return (async function* () { + yield step$1.createStep(name); + const awaitedResult = await result; + return awaitedResult; + })(); + } + return (function* () { + yield step$1.createStep(name); + return result; + })(); +} + +exports.step = step; diff --git a/docs/public/libs/v1/flow/step.d.ts b/docs/public/libs/v1/flow/step.d.ts new file mode 100644 index 000000000..d2295c3cc --- /dev/null +++ b/docs/public/libs/v1/flow/step.d.ts @@ -0,0 +1,39 @@ +import { type Step } from "./theFlow"; +/** + * Registers a named step in a flow and can optionally compute a value. + * + * **Supported call styles:** + * - Classic without callback: `step(name)` -> yields a step effect and returns `undefined` + * - Classic with callback: `step(name, theFunction)` -> yields a step effect and returns the callback result + * + * `step` records a named execution step that can be collected by `F.run(...)` when `includeDetails` is enabled. + * It can also wrap a synchronous or asynchronous callback and return its result while still emitting the step. + * Use it to make a flow easier to observe without changing its control flow. + * + * ```ts + * F.run( + * function *() { + * yield *F.step("load config"); + * return "done"; + * }, + * { includeDetails: true }, + * ); // { result: "done", steps: ["load config"] } + * + * F.run( + * function *() { + * const user = yield *F.step("read cache", () => "user-1"); + * return user; + * }, + * ); // "user-1" + * ``` + * + * @remarks + * - Steps are only collected in the final result when `includeDetails` is enabled + * + * @see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) For collecting step details + * @see https://utils.duplojs.dev/en/v1/api/flow/step + * + * @namespace F + * + */ +export declare function step(name: GenericName, theFunction?: () => GenericOutput): (GenericOutput extends Promise ? AsyncGenerator, Awaited> : Generator, GenericOutput>); diff --git a/docs/public/libs/v1/flow/step.mjs b/docs/public/libs/v1/flow/step.mjs new file mode 100644 index 000000000..e3fe79fbe --- /dev/null +++ b/docs/public/libs/v1/flow/step.mjs @@ -0,0 +1,21 @@ +import { createStep } from './theFlow/step.mjs'; + +/** + * {@include flow/step/index.md} + */ +function step(name, theFunction) { + const result = theFunction?.(); + if (result instanceof Promise) { + return (async function* () { + yield createStep(name); + const awaitedResult = await result; + return awaitedResult; + })(); + } + return (function* () { + yield createStep(name); + return result; + })(); +} + +export { step }; diff --git a/docs/public/libs/v1/flow/theFlow/break.cjs b/docs/public/libs/v1/flow/theFlow/break.cjs new file mode 100644 index 000000000..8872475cd --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/break.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const breakKind = kind.createFlowKind("break"); +function createBreak(value = undefined) { + return breakKind.setTo({}, { value }); +} + +exports.breakKind = breakKind; +exports.createBreak = createBreak; diff --git a/docs/public/libs/v1/flow/theFlow/break.d.ts b/docs/public/libs/v1/flow/theFlow/break.d.ts new file mode 100644 index 000000000..5cb4360b2 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/break.d.ts @@ -0,0 +1,7 @@ +import { type Kind } from "../../common"; +export declare const breakKind: import("../../common").KindHandler>; +export interface Break extends Kind { +} +export declare function createBreak(value?: GenericValue): Break; diff --git a/docs/public/libs/v1/flow/theFlow/break.mjs b/docs/public/libs/v1/flow/theFlow/break.mjs new file mode 100644 index 000000000..cedd3b6a1 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/break.mjs @@ -0,0 +1,8 @@ +import { createFlowKind } from '../kind.mjs'; + +const breakKind = createFlowKind("break"); +function createBreak(value = undefined) { + return breakKind.setTo({}, { value }); +} + +export { breakKind, createBreak }; diff --git a/docs/public/libs/v1/flow/theFlow/defer.cjs b/docs/public/libs/v1/flow/theFlow/defer.cjs new file mode 100644 index 000000000..ee2c8dee9 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/defer.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const deferKind = kind.createFlowKind("defer"); +function createDefer(value) { + return deferKind.setTo({}, value); +} + +exports.createDefer = createDefer; +exports.deferKind = deferKind; diff --git a/docs/public/libs/v1/flow/theFlow/defer.d.ts b/docs/public/libs/v1/flow/theFlow/defer.d.ts new file mode 100644 index 000000000..efb308fb2 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/defer.d.ts @@ -0,0 +1,5 @@ +import { type AnyFunction, type Kind } from "../../common"; +export declare const deferKind: import("../../common").KindHandler>>; +export interface Defer extends Kind GenericValue> { +} +export declare function createDefer(value: () => GenericOutput): Defer; diff --git a/docs/public/libs/v1/flow/theFlow/defer.mjs b/docs/public/libs/v1/flow/theFlow/defer.mjs new file mode 100644 index 000000000..084431b65 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/defer.mjs @@ -0,0 +1,8 @@ +import { createFlowKind } from '../kind.mjs'; + +const deferKind = createFlowKind("defer"); +function createDefer(value) { + return deferKind.setTo({}, value); +} + +export { createDefer, deferKind }; diff --git a/docs/public/libs/v1/flow/theFlow/dependence.cjs b/docs/public/libs/v1/flow/theFlow/dependence.cjs new file mode 100644 index 000000000..707626ed1 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/dependence.cjs @@ -0,0 +1,18 @@ +'use strict'; + +var kind = require('../kind.cjs'); +var pipe = require('../../common/pipe.cjs'); + +const dependenceHandlerKind = kind.createFlowKind("dependence-handler"); +/** + * {@include flow/createDependence/index.md} + */ +function createDependence(name) { + const dependenceHandler = function (implementation) { + return implementation; + }; + return pipe.pipe(dependenceHandler, (value) => dependenceHandlerKind.setTo(value, name)); +} + +exports.createDependence = createDependence; +exports.dependenceHandlerKind = dependenceHandlerKind; diff --git a/docs/public/libs/v1/flow/theFlow/dependence.d.ts b/docs/public/libs/v1/flow/theFlow/dependence.d.ts new file mode 100644 index 000000000..49283a783 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/dependence.d.ts @@ -0,0 +1,50 @@ +import { type Kind } from "../../common"; +export declare const dependenceHandlerKind: import("../../common").KindHandler>; +export interface DependenceHandlerKind extends Kind { +} +export type DependenceHandler = (DependenceHandlerKind & ((implementation: GenericType) => GenericType)); +export type DependenceHandlerDefinition = (DependenceHandlerKind & ((implementation: GenericType) => GenericType)); +/** + * Creates a typed dependency handler for the flow system. + * + * **Supported call styles:** + * - Classic: `createDependence(name)` -> returns a typed dependence handler definition + * + * `createDependence` creates a dependency descriptor identified by a string name. + * The returned handler is used with `inject(...)` and lets `run(...)` or `exec(...)` map a dependency bag to strongly typed values. + * At runtime, the handler also behaves like an identity function for the injected implementation. + * + * ```ts + * const database = F.createDependence("database"); + * + * F.run( + * function *() { + * const connection = yield *F.inject(database); + * return connection; + * }, + * { dependencies: { database: database("main-db") } }, + * ); // "main-db" + * + * const apiClient = F.createDependence("apiClient")<{ baseUrl: string }>; + * + * F.run( + * function *() { + * const client = yield *F.inject(apiClient); + * return client.baseUrl; + * }, + * { dependencies: { apiClient: apiClient({ baseUrl: "/api" }) } }, + * ); // "/api" + * + * database("replica-db"); // "replica-db" + * ``` + * + * @remarks + * - Use the returned dependence handler together with `inject(...)` + * + * @see [`F.inject`](https://utils.duplojs.dev/en/v1/api/flow/inject) To request the dependency inside a flow + * @see https://utils.duplojs.dev/en/v1/api/flow/createDependence + * + * @namespace F + * + */ +export declare function createDependence(name: GenericName): DependenceHandlerDefinition; diff --git a/docs/public/libs/v1/flow/theFlow/dependence.mjs b/docs/public/libs/v1/flow/theFlow/dependence.mjs new file mode 100644 index 000000000..c26491cbb --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/dependence.mjs @@ -0,0 +1,15 @@ +import { createFlowKind } from '../kind.mjs'; +import { pipe } from '../../common/pipe.mjs'; + +const dependenceHandlerKind = createFlowKind("dependence-handler"); +/** + * {@include flow/createDependence/index.md} + */ +function createDependence(name) { + const dependenceHandler = function (implementation) { + return implementation; + }; + return pipe(dependenceHandler, (value) => dependenceHandlerKind.setTo(value, name)); +} + +export { createDependence, dependenceHandlerKind }; diff --git a/docs/public/libs/v1/flow/theFlow/exit.cjs b/docs/public/libs/v1/flow/theFlow/exit.cjs new file mode 100644 index 000000000..d7d635c5f --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/exit.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const exitKind = kind.createFlowKind("exit"); +function createExit(value = undefined) { + return exitKind.setTo({}, { value }); +} + +exports.createExit = createExit; +exports.exitKind = exitKind; diff --git a/docs/public/libs/v1/flow/theFlow/exit.d.ts b/docs/public/libs/v1/flow/theFlow/exit.d.ts new file mode 100644 index 000000000..3022c9ab2 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/exit.d.ts @@ -0,0 +1,7 @@ +import { type Kind } from "../../common"; +export declare const exitKind: import("../../common").KindHandler>; +export interface Exit extends Kind { +} +export declare function createExit(value?: GenericValue): Exit; diff --git a/docs/public/libs/v1/flow/theFlow/exit.mjs b/docs/public/libs/v1/flow/theFlow/exit.mjs new file mode 100644 index 000000000..86fee677f --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/exit.mjs @@ -0,0 +1,8 @@ +import { createFlowKind } from '../kind.mjs'; + +const exitKind = createFlowKind("exit"); +function createExit(value = undefined) { + return exitKind.setTo({}, { value }); +} + +export { createExit, exitKind }; diff --git a/docs/public/libs/v1/flow/theFlow/finalizer.cjs b/docs/public/libs/v1/flow/theFlow/finalizer.cjs new file mode 100644 index 000000000..2c9d5e68f --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/finalizer.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const finalizerKind = kind.createFlowKind("finalizer"); +function createFinalizer(value) { + return finalizerKind.setTo({}, value); +} + +exports.createFinalizer = createFinalizer; +exports.finalizerKind = finalizerKind; diff --git a/docs/public/libs/v1/flow/theFlow/finalizer.d.ts b/docs/public/libs/v1/flow/theFlow/finalizer.d.ts new file mode 100644 index 000000000..2733a33c8 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/finalizer.d.ts @@ -0,0 +1,5 @@ +import { type AnyFunction, type Kind } from "../../common"; +export declare const finalizerKind: import("../../common").KindHandler>>; +export interface Finalizer extends Kind GenericValue> { +} +export declare function createFinalizer(value: () => GenericOutput): Finalizer; diff --git a/docs/public/libs/v1/flow/theFlow/finalizer.mjs b/docs/public/libs/v1/flow/theFlow/finalizer.mjs new file mode 100644 index 000000000..7d51cae86 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/finalizer.mjs @@ -0,0 +1,8 @@ +import { createFlowKind } from '../kind.mjs'; + +const finalizerKind = createFlowKind("finalizer"); +function createFinalizer(value) { + return finalizerKind.setTo({}, value); +} + +export { createFinalizer, finalizerKind }; diff --git a/docs/public/libs/v1/flow/theFlow/index.cjs b/docs/public/libs/v1/flow/theFlow/index.cjs new file mode 100644 index 000000000..9820c034a --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/index.cjs @@ -0,0 +1,14 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const theFLowKind = kind.createFlowKind("the-flow"); +/** + * {@include flow/create/index.md} + */ +function create(theFunction) { + return theFLowKind.setTo({}, { run: theFunction }); +} + +exports.create = create; +exports.theFLowKind = theFLowKind; diff --git a/docs/public/libs/v1/flow/theFlow/index.d.ts b/docs/public/libs/v1/flow/theFlow/index.d.ts new file mode 100644 index 000000000..00813ce35 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/index.d.ts @@ -0,0 +1,85 @@ +import { type GetKindValue, type Kind } from "../../common"; +import { type Step } from "./step"; +import { type Exit } from "./exit"; +import { type Injection } from "./injection"; +import { type Break } from "./break"; +import { type Defer } from "./defer"; +import { type Finalizer } from "./finalizer"; +import { type DependenceHandler, type dependenceHandlerKind } from "./dependence"; +export * from "./step"; +export * from "./exit"; +export * from "./break"; +export * from "./injection"; +export * from "./defer"; +export * from "./finalizer"; +export * from "./dependence"; +export type Effect = (Injection | Step | Exit | Break | Defer | Finalizer); +export type TheFlowGenerator = (Generator | AsyncGenerator); +export type TheFlowFunction = (input: GenericInput) => GenericGenerator; +export interface TheFlowProperties { + run: GenericFunction; +} +export declare const theFLowKind: import("../../common").KindHandler>>>>; +export interface TheFlow extends Kind> { +} +/** + * Creates a reusable flow object from a flow function. + * + * **Supported call styles:** + * - Classic: `create(theFunction)` -> returns a flow instance that can be passed to `F.run(...)` or `F.exec(...)` + * + * `create` wraps a generator-based flow function into a flow object understood by the flow runtime. + * The returned flow can be executed multiple times with different inputs and can be composed with `F.exec(...)`. + * Use it to name, share, and reuse flow definitions without executing them immediately. + * + * ```ts + * const greetingFlow = F.create( + * function *(name: string) { + * return `hello ${name}`; + * }, + * ); + * + * F.run(greetingFlow, { input: "Ada" }); // "hello Ada" + * + * const breakableFlow = F.create( + * function *(value: number) { + * yield *F.breakIf(value, (current) => current === 0); + * return value * 2; + * }, + * ); + * + * F.run(breakableFlow, { input: 0 }); // 0 + * + * F.run( + * function *() { + * return yield *F.exec(greetingFlow, { input: "Linus" }); + * }, + * ); // "hello Linus" + * + * const asyncFlow = F.create( + * async function *(name: string) { + * const value = await name.toUpperCase(); + * return value; + * }, + * ); + * + * await F.run(asyncFlow, { input: "flow" }); // Promise<"FLOW"> + * ``` + * + * @remarks + * - `create` does not execute the flow, it only wraps it for later use + * + * @see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) To execute a created flow + * @see [`F.exec`](https://utils.duplojs.dev/en/v1/api/flow/exec) To execute a created flow inside another flow + * @see https://utils.duplojs.dev/en/v1/api/flow/create + * + * @namespace F + * + */ +export declare function create(theFunction: GenericTheFlowFunction): TheFlow; +export type FlowInput = GenericFlow extends TheFlow ? InferredFunction extends TheFlowFunction ? InferredInput : never : never; +export type WrapFlow = GenericFlow extends TheFlowGenerator ? TheFlow> : GenericFlow extends TheFlowFunction ? TheFlow : GenericFlow; +export type FlowDependencies = (ExtractFlowGenerator extends TheFlowGenerator ? InferredEffect extends Injection ? InferredDependenceHandler : never : never) extends infer InferredDependenceHandler extends DependenceHandler ? { + [Dependence in InferredDependenceHandler as Extract, string>]: ReturnType; +} : never; +export type ExtractFlowGenerator = GenericFlow extends TheFlow ? InferredFunction extends TheFlowFunction ? InferredGenerator : never : never; diff --git a/docs/public/libs/v1/flow/theFlow/index.mjs b/docs/public/libs/v1/flow/theFlow/index.mjs new file mode 100644 index 000000000..299cd4efe --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/index.mjs @@ -0,0 +1,11 @@ +import { createFlowKind } from '../kind.mjs'; + +const theFLowKind = createFlowKind("the-flow"); +/** + * {@include flow/create/index.md} + */ +function create(theFunction) { + return theFLowKind.setTo({}, { run: theFunction }); +} + +export { create, theFLowKind }; diff --git a/docs/public/libs/v1/flow/theFlow/injection.cjs b/docs/public/libs/v1/flow/theFlow/injection.cjs new file mode 100644 index 000000000..36d14cc12 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/injection.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const injectionKind = kind.createFlowKind("injection"); +function createInjection(properties) { + return injectionKind.setTo({}, properties); +} + +exports.createInjection = createInjection; +exports.injectionKind = injectionKind; diff --git a/docs/public/libs/v1/flow/theFlow/injection.d.ts b/docs/public/libs/v1/flow/theFlow/injection.d.ts new file mode 100644 index 000000000..029952a54 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/injection.d.ts @@ -0,0 +1,10 @@ +import { type Kind } from "../../common"; +import { type DependenceHandler } from "./dependence"; +export interface InjectionProperties { + dependenceHandler: GenericDependenceHandler; + inject(value: ReturnType): void; +} +export declare const injectionKind: import("../../common").KindHandler>>; +export interface Injection extends Kind> { +} +export declare function createInjection(properties: InjectionProperties): Injection; diff --git a/docs/public/libs/v1/flow/theFlow/injection.mjs b/docs/public/libs/v1/flow/theFlow/injection.mjs new file mode 100644 index 000000000..8d89c304c --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/injection.mjs @@ -0,0 +1,8 @@ +import { createFlowKind } from '../kind.mjs'; + +const injectionKind = createFlowKind("injection"); +function createInjection(properties) { + return injectionKind.setTo({}, properties); +} + +export { createInjection, injectionKind }; diff --git a/docs/public/libs/v1/flow/theFlow/step.cjs b/docs/public/libs/v1/flow/theFlow/step.cjs new file mode 100644 index 000000000..526220596 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/step.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var kind = require('../kind.cjs'); + +const stepKind = kind.createFlowKind("step"); +function createStep(name) { + return stepKind.setTo({}, name); +} + +exports.createStep = createStep; +exports.stepKind = stepKind; diff --git a/docs/public/libs/v1/flow/theFlow/step.d.ts b/docs/public/libs/v1/flow/theFlow/step.d.ts new file mode 100644 index 000000000..b63641a71 --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/step.d.ts @@ -0,0 +1,5 @@ +import { type Kind } from "../../common"; +export declare const stepKind: import("../../common").KindHandler>; +export interface Step extends Kind { +} +export declare function createStep(name: GenericName): Step; diff --git a/docs/public/libs/v1/flow/theFlow/step.mjs b/docs/public/libs/v1/flow/theFlow/step.mjs new file mode 100644 index 000000000..e8b557bdb --- /dev/null +++ b/docs/public/libs/v1/flow/theFlow/step.mjs @@ -0,0 +1,8 @@ +import { createFlowKind } from '../kind.mjs'; + +const stepKind = createFlowKind("step"); +function createStep(name) { + return stepKind.setTo({}, name); +} + +export { createStep, stepKind }; diff --git a/docs/public/libs/v1/flow/types/index.d.ts b/docs/public/libs/v1/flow/types/index.d.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/docs/public/libs/v1/flow/types/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/docs/public/libs/v1/index.cjs b/docs/public/libs/v1/index.cjs index 553cc2d63..922e0303f 100644 --- a/docs/public/libs/v1/index.cjs +++ b/docs/public/libs/v1/index.cjs @@ -12,6 +12,7 @@ var index$8 = require('./dataParser/parsers/coerce/index.cjs'); var index$9 = require('./dataParser/extended/index.cjs'); var index$a = require('./date/index.cjs'); var index$b = require('./clean/index.cjs'); +var index$c = require('./flow/index.cjs'); var addWrappedProperties = require('./common/addWrappedProperties.cjs'); var asyncPipe = require('./common/asyncPipe.cjs'); var clone = require('./common/clone.cjs'); @@ -93,6 +94,8 @@ exports.D = index$a; exports.DDate = index$a; exports.C = index$b; exports.DClean = index$b; +exports.DFlow = index$c; +exports.F = index$c; exports.addWrappedProperties = addWrappedProperties.addWrappedProperties; exports.asyncPipe = asyncPipe.asyncPipe; exports.clone = clone.clone; diff --git a/docs/public/libs/v1/index.d.ts b/docs/public/libs/v1/index.d.ts index a0fa406c7..9ded4f6c5 100644 --- a/docs/public/libs/v1/index.d.ts +++ b/docs/public/libs/v1/index.d.ts @@ -23,3 +23,5 @@ export * as D from "./date"; export * as DDate from "./date"; export * as C from "./clean"; export * as DClean from "./clean"; +export * as F from "./flow"; +export * as DFlow from "./flow"; diff --git a/docs/public/libs/v1/index.mjs b/docs/public/libs/v1/index.mjs index db568311b..17b1dc04f 100644 --- a/docs/public/libs/v1/index.mjs +++ b/docs/public/libs/v1/index.mjs @@ -34,6 +34,9 @@ export { index$a as DDate }; import * as index$b from './clean/index.mjs'; export { index$b as C }; export { index$b as DClean }; +import * as index$c from './flow/index.mjs'; +export { index$c as DFlow }; +export { index$c as F }; export { addWrappedProperties } from './common/addWrappedProperties.mjs'; export { asyncPipe } from './common/asyncPipe.mjs'; export { clone } from './common/clone.mjs'; diff --git a/docs/public/libs/v1/metadata.json b/docs/public/libs/v1/metadata.json index d8c20ff24..cb7b43e55 100644 --- a/docs/public/libs/v1/metadata.json +++ b/docs/public/libs/v1/metadata.json @@ -3902,6 +3902,195 @@ } ] }, + { + "name": "flow", + "files": [ + { + "name": "theFlow", + "files": [ + { + "name": "break.cjs" + }, + { + "name": "break.d.ts" + }, + { + "name": "break.mjs" + }, + { + "name": "defer.cjs" + }, + { + "name": "defer.d.ts" + }, + { + "name": "defer.mjs" + }, + { + "name": "dependence.cjs" + }, + { + "name": "dependence.d.ts" + }, + { + "name": "dependence.mjs" + }, + { + "name": "exit.cjs" + }, + { + "name": "exit.d.ts" + }, + { + "name": "exit.mjs" + }, + { + "name": "finalizer.cjs" + }, + { + "name": "finalizer.d.ts" + }, + { + "name": "finalizer.mjs" + }, + { + "name": "index.cjs" + }, + { + "name": "index.d.ts" + }, + { + "name": "index.mjs" + }, + { + "name": "injection.cjs" + }, + { + "name": "injection.d.ts" + }, + { + "name": "injection.mjs" + }, + { + "name": "step.cjs" + }, + { + "name": "step.d.ts" + }, + { + "name": "step.mjs" + } + ] + }, + { + "name": "types", + "files": [ + { + "name": "index.d.ts" + } + ] + }, + { + "name": "breakIf.cjs" + }, + { + "name": "breakIf.d.ts" + }, + { + "name": "breakIf.mjs" + }, + { + "name": "defer.cjs" + }, + { + "name": "defer.d.ts" + }, + { + "name": "defer.mjs" + }, + { + "name": "exec.cjs" + }, + { + "name": "exec.d.ts" + }, + { + "name": "exec.mjs" + }, + { + "name": "exitIf.cjs" + }, + { + "name": "exitIf.d.ts" + }, + { + "name": "exitIf.mjs" + }, + { + "name": "finalizer.cjs" + }, + { + "name": "finalizer.d.ts" + }, + { + "name": "finalizer.mjs" + }, + { + "name": "index.cjs" + }, + { + "name": "index.d.ts" + }, + { + "name": "index.mjs" + }, + { + "name": "initializer.cjs" + }, + { + "name": "initializer.d.ts" + }, + { + "name": "initializer.mjs" + }, + { + "name": "inject.cjs" + }, + { + "name": "inject.d.ts" + }, + { + "name": "inject.mjs" + }, + { + "name": "kind.cjs" + }, + { + "name": "kind.d.ts" + }, + { + "name": "kind.mjs" + }, + { + "name": "run.cjs" + }, + { + "name": "run.d.ts" + }, + { + "name": "run.mjs" + }, + { + "name": "step.cjs" + }, + { + "name": "step.d.ts" + }, + { + "name": "step.mjs" + } + ] + }, { "name": "generator", "files": [ diff --git a/docs/public/libs/v1/object/types/requireAtLeastOne.d.ts b/docs/public/libs/v1/object/types/requireAtLeastOne.d.ts index 4cecb2e48..1f0155ab7 100644 --- a/docs/public/libs/v1/object/types/requireAtLeastOne.d.ts +++ b/docs/public/libs/v1/object/types/requireAtLeastOne.d.ts @@ -1,7 +1,8 @@ import { type IsEqual } from "../../common"; declare const SymbolRequireAtLeastOneError: unique symbol; +declare const SymbolOneOf: unique symbol; export type RequireAtLeastOne = IsEqual, never> extends true ? { [SymbolRequireAtLeastOneError]: "requires at least one key."; - oneOf: GenericKeys; + [SymbolOneOf]: `key: ${Extract}`; } : unknown; export {}; diff --git a/eslint.config.js b/eslint.config.js index 47191d494..def406d89 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,6 +31,7 @@ export default [ "@typescript-eslint/no-confusing-void-expression": "off", "no-nested-ternary": "off", "@stylistic/js/line-comment-position": "off", + "require-yield": "off", }, }, { diff --git a/jsDoc/flow/breakIf/example.ts b/jsDoc/flow/breakIf/example.ts new file mode 100644 index 000000000..7f93ae3b6 --- /dev/null +++ b/jsDoc/flow/breakIf/example.ts @@ -0,0 +1,25 @@ +import { F } from "@scripts"; + +F.run( + function *() { + yield *F.breakIf(2, (value) => value === 2); + + return "test"; + }, +); // 2 + +F.run( + function *() { + const value = yield *F.breakIf("keep", (value) => value === "stop"); + return value; + }, +); // "keep" + +F.run( + function *() { + yield *F.step("before break"); + yield *F.breakIf(2, (value) => value === 2); + return "done"; + }, + { includeDetails: true }, +); // { result: 2, steps: ["before break"] } diff --git a/jsDoc/flow/breakIf/index.md b/jsDoc/flow/breakIf/index.md new file mode 100644 index 000000000..d65245197 --- /dev/null +++ b/jsDoc/flow/breakIf/index.md @@ -0,0 +1,21 @@ +Breaks a flow when a predicate matches a value. + +**Supported call styles:** +- Classic boolean predicate: `breakIf(value, thePredicate)` -> yields a break effect or returns the value +- Classic predicate overload: `breakIf(value, thePredicate)` -> narrows the matched value type before breaking + +`breakIf` is designed to be used inside `F.run(...)` or a nested flow executed by `F.exec(...)`. +When the predicate returns `true`, the current flow stops with the provided value as a break result. +When the predicate returns `false`, the original value is returned and the flow continues normally. + +```ts +{@include flow/breakIf/example.ts[3,25]} +``` + +@remarks +- Use `breakIf` to stop the current flow without exiting outer flows + +@see [`F.exitIf`](https://utils.duplojs.dev/en/v1/api/flow/exitIf) To exit a flow instead of breaking it +@see https://utils.duplojs.dev/en/v1/api/flow/breakIf + +@namespace F diff --git a/jsDoc/flow/create/example.ts b/jsDoc/flow/create/example.ts new file mode 100644 index 000000000..d6d05529b --- /dev/null +++ b/jsDoc/flow/create/example.ts @@ -0,0 +1,34 @@ +/* eslint-disable require-yield */ +import { F } from "@scripts"; + +const greetingFlow = F.create( + function *(name: string) { + return `hello ${name}`; + }, +); + +F.run(greetingFlow, { input: "Ada" }); // "hello Ada" + +const breakableFlow = F.create( + function *(value: number) { + yield *F.breakIf(value, (current) => current === 0); + return value * 2; + }, +); + +F.run(breakableFlow, { input: 0 }); // 0 + +F.run( + function *() { + return yield *F.exec(greetingFlow, { input: "Linus" }); + }, +); // "hello Linus" + +const asyncFlow = F.create( + async function *(name: string) { + const value = await name.toUpperCase(); + return value; + }, +); + +await F.run(asyncFlow, { input: "flow" }); // Promise<"FLOW"> diff --git a/jsDoc/flow/create/index.md b/jsDoc/flow/create/index.md new file mode 100644 index 000000000..1198a68b0 --- /dev/null +++ b/jsDoc/flow/create/index.md @@ -0,0 +1,21 @@ +Creates a reusable flow object from a flow function. + +**Supported call styles:** +- Classic: `create(theFunction)` -> returns a flow instance that can be passed to `F.run(...)` or `F.exec(...)` + +`create` wraps a generator-based flow function into a flow object understood by the flow runtime. +The returned flow can be executed multiple times with different inputs and can be composed with `F.exec(...)`. +Use it to name, share, and reuse flow definitions without executing them immediately. + +```ts +{@include flow/create/example.ts[4,34]} +``` + +@remarks +- `create` does not execute the flow, it only wraps it for later use + +@see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) To execute a created flow +@see [`F.exec`](https://utils.duplojs.dev/en/v1/api/flow/exec) To execute a created flow inside another flow +@see https://utils.duplojs.dev/en/v1/api/flow/create + +@namespace F diff --git a/jsDoc/flow/createDependence/example.ts b/jsDoc/flow/createDependence/example.ts new file mode 100644 index 000000000..78b5956ee --- /dev/null +++ b/jsDoc/flow/createDependence/example.ts @@ -0,0 +1,23 @@ +import { F } from "@scripts"; + +const database = F.createDependence("database"); + +F.run( + function *() { + const connection = yield *F.inject(database); + return connection; + }, + { dependencies: { database: database("main-db") } }, +); // "main-db" + +const apiClient = F.createDependence("apiClient")<{ baseUrl: string }>; + +F.run( + function *() { + const client = yield *F.inject(apiClient); + return client.baseUrl; + }, + { dependencies: { apiClient: apiClient({ baseUrl: "/api" }) } }, +); // "/api" + +database("replica-db"); // "replica-db" diff --git a/jsDoc/flow/createDependence/index.md b/jsDoc/flow/createDependence/index.md new file mode 100644 index 000000000..ccf59ee97 --- /dev/null +++ b/jsDoc/flow/createDependence/index.md @@ -0,0 +1,20 @@ +Creates a typed dependency handler for the flow system. + +**Supported call styles:** +- Classic: `createDependence(name)` -> returns a typed dependence handler definition + +`createDependence` creates a dependency descriptor identified by a string name. +The returned handler is used with `inject(...)` and lets `run(...)` or `exec(...)` map a dependency bag to strongly typed values. +At runtime, the handler also behaves like an identity function for the injected implementation. + +```ts +{@include flow/createDependence/example.ts[3,23]} +``` + +@remarks +- Use the returned dependence handler together with `inject(...)` + +@see [`F.inject`](https://utils.duplojs.dev/en/v1/api/flow/inject) To request the dependency inside a flow +@see https://utils.duplojs.dev/en/v1/api/flow/createDependence + +@namespace F diff --git a/jsDoc/flow/createInitializer/example.ts b/jsDoc/flow/createInitializer/example.ts new file mode 100644 index 000000000..0cfea25ff --- /dev/null +++ b/jsDoc/flow/createInitializer/example.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/require-await */ +import { F } from "@scripts"; +import { createInitializer } from "@scripts/flow/initializer"; + +const userInitializer = createInitializer( + (name: string) => ({ name }), + { defer: (user) => void console.log(`close:${user.name}`) }, +); + +F.run( + function *() { + return yield *userInitializer("Ada"); + }, +); // { name: "Ada" } + +const finalizerLogs: string[] = []; +const tokenInitializer = createInitializer( + (id: number) => `token-${id}`, + { finalizer: (token) => finalizerLogs.push(token) }, +); + +F.run( + function *() { + return yield *tokenInitializer(42); + }, +); // "token-42" + +const asyncInitializer = createInitializer( + (name: string) => Promise.resolve({ + name, + ready: true, + }), + { defer: (user) => void console.log(`async:${user.name}`) }, +); + +void await F.run( + async function *() { + const value = yield *asyncInitializer("Linus"); + // Promise<{ name: string; ready: true }> + + return; + }, +); diff --git a/jsDoc/flow/createInitializer/index.md b/jsDoc/flow/createInitializer/index.md new file mode 100644 index 000000000..b047ee0be --- /dev/null +++ b/jsDoc/flow/createInitializer/index.md @@ -0,0 +1,21 @@ +Creates an initializer that returns a value and automatically registers flow cleanup effects. + +**Supported call styles:** +- Classic: `createInitializer(initializer, params)` -> returns a function that can be yielded inside a flow + +`createInitializer` wraps an initializer function and turns its result into a flow-friendly generator. +Depending on the provided options, it can register a `defer` callback, a `finalizer` callback, or both, using the produced value. +The returned initializer can then be executed inside `F.run(...)` like any other flow generator. + +```ts +{@include flow/createInitializer/example.ts[5,43]} +``` + +@remarks +- `createInitializer` is useful when a setup step should also declare matching cleanup logic + +@see [`F.defer`](https://utils.duplojs.dev/en/v1/api/flow/defer) For cleanup callbacks +@see [`F.finalizer`](https://utils.duplojs.dev/en/v1/api/flow/finalizer) For final callbacks +@see https://utils.duplojs.dev/en/v1/api/flow/createInitializer + +@namespace F diff --git a/jsDoc/flow/defer/example.ts b/jsDoc/flow/defer/example.ts new file mode 100644 index 000000000..d058de815 --- /dev/null +++ b/jsDoc/flow/defer/example.ts @@ -0,0 +1,25 @@ +import { F } from "@scripts"; + +F.run( + function *() { + yield *F.defer(() => void console.log("close connection")); + return "done"; + }, +); // "done" + +F.run( + function *() { + yield *F.defer(() => void console.log("clear cache")); + yield *F.breakIf(2, (value) => value === 2); + return "done"; + }, +); // 2 + +await F.run( + async function *() { + yield *F.defer(async() => { + await Promise.resolve(); + }); + return Promise.resolve("done"); + }, +); // Promise<"done"> diff --git a/jsDoc/flow/defer/index.md b/jsDoc/flow/defer/index.md new file mode 100644 index 000000000..125f2d276 --- /dev/null +++ b/jsDoc/flow/defer/index.md @@ -0,0 +1,20 @@ +Registers a cleanup callback that runs when the flow finishes. + +**Supported call styles:** +- Classic: `defer(theFunction)` -> yields a defer effect + +`defer` stores a callback that is executed after the flow has completed. +It is useful for releasing resources, closing handles, or running cleanup logic after a `break` or a normal return. +Use it inside `F.run(...)` or inside subflows executed by `F.exec(...)`. + +```ts +{@include flow/defer/example.ts[3,25]} +``` + +@remarks +- Deferred callbacks run after the flow result has been computed + +@see [`F.finalizer`](https://utils.duplojs.dev/en/v1/api/flow/finalizer) To register final logic in the same flow system +@see https://utils.duplojs.dev/en/v1/api/flow/defer + +@namespace F diff --git a/jsDoc/flow/exec/example.ts b/jsDoc/flow/exec/example.ts new file mode 100644 index 000000000..fc28a003e --- /dev/null +++ b/jsDoc/flow/exec/example.ts @@ -0,0 +1,39 @@ +/* eslint-disable require-yield */ +import { F } from "@scripts"; + +const upperCaseFlow = F.create( + function *(input: string) { + return input.toUpperCase(); + }, +); + +const userFlow = F.create( + function *(id: number) { + return `user-${id}`; + }, +); + +const breakableFlow = F.create( + function *(value: number) { + yield *F.breakIf(value, (current) => current === 2); + return "done"; + }, +); + +F.run( + function *() { + return yield *F.exec(upperCaseFlow, { input: "hello" }); + }, +); // "HELLO" + +F.run( + function *() { + return yield *F.exec(userFlow, { input: 42 }); + }, +); // "user-42" + +F.run( + function *() { + return yield *F.exec(breakableFlow, { input: 2 }); + }, +); // 2 diff --git a/jsDoc/flow/exec/index.md b/jsDoc/flow/exec/index.md new file mode 100644 index 000000000..d595f9a21 --- /dev/null +++ b/jsDoc/flow/exec/index.md @@ -0,0 +1,22 @@ +Executes a nested flow inside the current flow. + +**Supported call styles:** +- Classic with a flow function: `exec(theFlow, params?)` -> runs the provided flow function +- Classic with a flow instance: `exec(theFlow, params?)` -> runs a flow created with `F.create(...)` +- Classic with a generator: `exec(theGenerator, params?)` -> runs an existing generator directly + +`exec` lets a parent flow call another flow while staying in the same execution model. +Break values are converted into the local return value of `exec`, while exit, step, injection, and finalizer effects are forwarded to the outer flow. +It can be used in synchronous and asynchronous flows. + +```ts +{@include flow/exec/example.ts[3,38]} +``` + +@remarks +- `exec` is useful for composing small flows into larger ones + +@see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) To execute the root flow +@see https://utils.duplojs.dev/en/v1/api/flow/exec + +@namespace F diff --git a/jsDoc/flow/exitIf/example.ts b/jsDoc/flow/exitIf/example.ts new file mode 100644 index 000000000..7bf6489a8 --- /dev/null +++ b/jsDoc/flow/exitIf/example.ts @@ -0,0 +1,41 @@ +import { F } from "@scripts"; + +const thirdLevelFlow = F.create( + function *() { + yield *F.exitIf("stop", (value) => value === "stop"); + return "done"; + }, +); + +const secondLevelFlow = F.create( + function *() { + return yield *F.exec(thirdLevelFlow); + }, +); + +const firstLevelFlow = F.create( + function *() { + return yield *F.exec(secondLevelFlow); + }, +); + +F.run( + function *() { + return yield *F.exitIf(2, (value) => value === 2); + }, +); // 2 + +F.run( + function *() { + const value = yield *F.exitIf("keep", (value) => value === "stop"); + return value; + }, +); // "keep" + +F.run( + function *() { + yield *F.step("before deep exit"); + return yield *F.exec(firstLevelFlow); + }, + { includeDetails: true }, +); // { result: "stop", steps: ["before deep exit"] } diff --git a/jsDoc/flow/exitIf/index.md b/jsDoc/flow/exitIf/index.md new file mode 100644 index 000000000..e3ebdc56b --- /dev/null +++ b/jsDoc/flow/exitIf/index.md @@ -0,0 +1,22 @@ +Exits a flow when a predicate matches a value. + +**Supported call styles:** +- Classic boolean predicate: `exitIf(value, thePredicate)` -> yields an exit effect or returns the value +- Classic predicate overload: `exitIf(value, thePredicate)` -> narrows the matched value type before exiting + +`exitIf` is designed to be used inside `F.run(...)` or a nested flow executed by `F.exec(...)`. +When the predicate returns `true`, the current flow exits with the provided value. +When the predicate returns `false`, the original value is returned and the flow continues normally. +Because the exit effect is forwarded through nested `exec(...)` calls, it can stop a deeply nested flow from any depth. + +```ts +{@include flow/exitIf/example.ts[3,41]} +``` + +@remarks +- Use `exitIf` when the whole running flow should stop with a value + +@see [`F.breakIf`](https://utils.duplojs.dev/en/v1/api/flow/breakIf) To stop only the current local flow branch +@see https://utils.duplojs.dev/en/v1/api/flow/exitIf + +@namespace F diff --git a/jsDoc/flow/finalizer/example.ts b/jsDoc/flow/finalizer/example.ts new file mode 100644 index 000000000..d61afa7c4 --- /dev/null +++ b/jsDoc/flow/finalizer/example.ts @@ -0,0 +1,25 @@ +import { F } from "@scripts"; + +F.run( + function *() { + yield *F.finalizer(() => void console.log("close connection")); + return "done"; + }, +); // "done" + +F.run( + function *() { + yield *F.finalizer(() => void console.log("clear cache")); + yield *F.breakIf(2, (value) => value === 2); + return "done"; + }, +); // 2 + +await F.run( + async function *() { + yield *F.finalizer(async() => { + await Promise.resolve(); + }); + return Promise.resolve("done"); + }, +); // Promise<"done"> diff --git a/jsDoc/flow/finalizer/index.md b/jsDoc/flow/finalizer/index.md new file mode 100644 index 000000000..a31bb06df --- /dev/null +++ b/jsDoc/flow/finalizer/index.md @@ -0,0 +1,20 @@ +Registers a final callback handled by the flow runner. + +**Supported call styles:** +- Classic: `finalizer(theFunction)` -> yields a finalizer effect + +`finalizer` registers logic that is executed by the flow runner when the flow completes. +It is useful for cleanup or post-processing that should stay inside the flow effect system. +Use it inside `F.run(...)` or inside subflows executed by `F.exec(...)`. + +```ts +{@include flow/finalizer/example.ts[3,25]} +``` + +@remarks +- Finalizers are collected by the flow runner and executed after the flow ends + +@see [`F.defer`](https://utils.duplojs.dev/en/v1/api/flow/defer) For another cleanup-oriented effect +@see https://utils.duplojs.dev/en/v1/api/flow/finalizer + +@namespace F diff --git a/jsDoc/flow/inject/example.ts b/jsDoc/flow/inject/example.ts new file mode 100644 index 000000000..ea362f99e --- /dev/null +++ b/jsDoc/flow/inject/example.ts @@ -0,0 +1,24 @@ +import { F } from "@scripts"; + +const database = F.createDependence("database"); + +F.run( + function *() { + const connection = yield *F.inject(database); + return connection; + }, + { dependencies: { database: "main-db" } }, +); // "main-db" + +F.run( + function *() { + return yield *F.exec( + function *() { + const connection = yield *F.inject(database); + return connection; + }, + { dependencies: { database: "replica-db" } }, + ); + }, + { dependencies: { database: "main-db" } }, +); // "replica-db" diff --git a/jsDoc/flow/inject/index.md b/jsDoc/flow/inject/index.md new file mode 100644 index 000000000..7b83a17be --- /dev/null +++ b/jsDoc/flow/inject/index.md @@ -0,0 +1,20 @@ +Requests a dependency from the flow runner. + +**Supported call styles:** +- Classic: `inject(dependenceHandler)` -> yields an injection effect and returns the injected value + +`inject` lets a flow declare that it needs a dependency by using a dependence handler created with `F.createDependence(...)`. +When `F.run(...)` or `F.exec(...)` receives matching dependencies, the requested value is injected back into the flow. +If the dependency is missing, the runner throws a missing dependence error. + +```ts +{@include flow/inject/example.ts[3,24]} +``` + +@remarks +- `inject` keeps dependencies explicit in flow definitions + +@see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) For providing dependencies +@see https://utils.duplojs.dev/en/v1/api/flow/inject + +@namespace F diff --git a/jsDoc/flow/run/example.ts b/jsDoc/flow/run/example.ts new file mode 100644 index 000000000..e1cd8a084 --- /dev/null +++ b/jsDoc/flow/run/example.ts @@ -0,0 +1,29 @@ +/* eslint-disable require-yield */ +import { F } from "@scripts"; + +F.run( + function *(input: string) { + return input.toUpperCase(); + }, + { input: "hello" }, +); // "HELLO" + +F.run( + function *() { + yield *F.step("check cache"); + yield *F.breakIf(2, (value) => value === 2); + return "done"; + }, + { includeDetails: true }, +); // { result: 2, steps: ["check cache"] } + +const service = F.createDependence("service"); + +F.run( + function *() { + const currentService = yield *F.inject(service); + yield *F.finalizer(() => currentService.toUpperCase()); + return currentService; + }, + { dependencies: { service: "api" } }, +); // "api" diff --git a/jsDoc/flow/run/index.md b/jsDoc/flow/run/index.md new file mode 100644 index 000000000..6e654237d --- /dev/null +++ b/jsDoc/flow/run/index.md @@ -0,0 +1,21 @@ +Runs a flow and resolves its final result. + +**Supported call styles:** +- Classic with a flow function: `run(theFlow, params?)` -> executes the provided flow function +- Classic with a flow instance: `run(theFlow, params?)` -> executes a flow created with `F.create(...)` + +`run` is the entry point of the flow system. +It executes synchronous or asynchronous flows, handles break and exit effects, collects steps when `includeDetails` is enabled, runs deferred and finalizer callbacks, and injects declared dependencies. +Use `run` to start a top-level flow and get its final value. + +```ts +{@include flow/run/example.ts[4,29]} +``` + +@remarks +- `run` returns a `Promise` when the executed flow is asynchronous + +@see [`F.exec`](https://utils.duplojs.dev/en/v1/api/flow/exec) To run a nested flow from inside another flow +@see https://utils.duplojs.dev/en/v1/api/flow/run + +@namespace F diff --git a/jsDoc/flow/step/example.ts b/jsDoc/flow/step/example.ts new file mode 100644 index 000000000..6bccc5b15 --- /dev/null +++ b/jsDoc/flow/step/example.ts @@ -0,0 +1,16 @@ +import { F } from "@scripts"; + +F.run( + function *() { + yield *F.step("load config"); + return "done"; + }, + { includeDetails: true }, +); // { result: "done", steps: ["load config"] } + +F.run( + function *() { + const user = yield *F.step("read cache", () => "user-1"); + return user; + }, +); // "user-1" diff --git a/jsDoc/flow/step/index.md b/jsDoc/flow/step/index.md new file mode 100644 index 000000000..568ad768b --- /dev/null +++ b/jsDoc/flow/step/index.md @@ -0,0 +1,21 @@ +Registers a named step in a flow and can optionally compute a value. + +**Supported call styles:** +- Classic without callback: `step(name)` -> yields a step effect and returns `undefined` +- Classic with callback: `step(name, theFunction)` -> yields a step effect and returns the callback result + +`step` records a named execution step that can be collected by `F.run(...)` when `includeDetails` is enabled. +It can also wrap a synchronous or asynchronous callback and return its result while still emitting the step. +Use it to make a flow easier to observe without changing its control flow. + +```ts +{@include flow/step/example.ts[3,16]} +``` + +@remarks +- Steps are only collected in the final result when `includeDetails` is enabled + +@see [`F.run`](https://utils.duplojs.dev/en/v1/api/flow/run) For collecting step details +@see https://utils.duplojs.dev/en/v1/api/flow/step + +@namespace F diff --git a/package.json b/package.json index 1a91958fe..7390c050e 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,11 @@ "import": "./dist/clean/index.mjs", "require": "./dist/clean/index.cjs", "types": "./dist/clean/index.d.ts" + }, + "./flow": { + "import": "./dist/flow/index.mjs", + "require": "./dist/flow/index.cjs", + "types": "./dist/flow/index.d.ts" } }, "files": [ diff --git a/scripts/common/kind.ts b/scripts/common/kind.ts index 16bf726e7..27b0befd9 100644 --- a/scripts/common/kind.ts +++ b/scripts/common/kind.ts @@ -160,7 +160,10 @@ export function createKind< }, has(input): input is never { return input - && typeof input === "object" + && ( + typeof input === "object" + || typeof input === "function" + ) && runTimeKey in input; }, getValue(input) { @@ -176,6 +179,7 @@ export interface ReservedKindNamespace { DuplojsUtilsError: true; DuplojsUtilsClean: true; DuplojsUtilsDate: true; + DuplojsUtilsFlow: true; } type ForbiddenKindNamespace< diff --git a/scripts/dataParser/base.ts b/scripts/dataParser/base.ts index 0b961a7cb..a87169028 100644 --- a/scripts/dataParser/base.ts +++ b/scripts/dataParser/base.ts @@ -1,4 +1,4 @@ -import { type AnyFunction, createErrorKind, createOverride, type GetKind, type GetKindHandler, type GetKindValue, type IsEqual, keyWrappedValue, type Kind, type KindHandler, kindHeritage, type OverrideHandler, pipe, type RemoveKind, simpleClone, type SimplifyTopLevel } from "@scripts/common"; +import { type AnyFunction, createErrorKind, createOverride, type GetKind, type GetKindHandler, type GetKindValue, type IsEqual, keyWrappedValue, type Kind, type KindHandler, kindHeritage, type OverrideHandler, pipe, type RemoveKind, simpleClone } from "@scripts/common"; import { addIssue, createError, SymbolDataParserErrorIssue, SymbolDataParserErrorPromiseIssue, type DataParserError, addPromiseIssue } from "./error"; import * as DEither from "@scripts/either"; import { createDataParserKind } from "./kind"; diff --git a/scripts/flow/breakIf.ts b/scripts/flow/breakIf.ts new file mode 100644 index 000000000..910f1a504 --- /dev/null +++ b/scripts/flow/breakIf.ts @@ -0,0 +1,34 @@ +import { type NeverCoalescing, type AnyFunction } from "@scripts/common"; +import { type Break, createBreak } from "./theFlow"; + +/** + * {@include flow/breakIf/index.md} + */ +export function breakIf< + GenericValue extends unknown, + GenericPredicate extends GenericValue, +>( + value: GenericValue, + thePredicate: (value: GenericValue) => value is GenericPredicate +): Generator< + Break< + NeverCoalescing, GenericPredicate> + >, + Exclude +>; +export function breakIf< + GenericValue extends unknown, +>( + value: GenericValue, + thePredicate: (value: GenericValue) => boolean +): Generator< + Break, + GenericValue +>; +export function *breakIf(value: unknown, thePredicate: AnyFunction): any { + if (thePredicate(value) === true) { + yield createBreak(value); + } else { + return value; + } +} diff --git a/scripts/flow/defer.ts b/scripts/flow/defer.ts new file mode 100644 index 000000000..dd539e1a1 --- /dev/null +++ b/scripts/flow/defer.ts @@ -0,0 +1,19 @@ +import { createDefer, type Defer } from "./theFlow"; + +/** + * {@include flow/defer/index.md} + */ +export function *defer< + GenericOutput extends unknown, +>( + theFunction: () => GenericOutput, +): ( + | Generator, undefined> + | ( + GenericOutput extends Promise + ? AsyncGenerator, undefined> + : never + ) + ) { + yield createDefer(theFunction); +} diff --git a/scripts/flow/exec.ts b/scripts/flow/exec.ts new file mode 100644 index 000000000..ebb57a0f6 --- /dev/null +++ b/scripts/flow/exec.ts @@ -0,0 +1,201 @@ +import { justExec, type SimplifyTopLevel, type IsEqual, type IsExtends, type Or, forward } from "@scripts/common"; +import { type TheFlowGenerator, type TheFlow, type TheFlowFunction, type FlowInput, type WrapFlow, type Exit, type Break, type Injection, theFlowKind, exitKind, breakKind, type Step, stepKind, type FlowDependencies, type Effect, injectionKind, dependenceHandlerKind } from "./theFlow"; +import { deferKind } from "./theFlow/defer"; +import { type Finalizer, finalizerKind } from "./theFlow/finalizer"; + +type ComputeExecParams< + GenericInput extends unknown, + GenericDependencies extends Record, +> = SimplifyTopLevel< + & ( + Or<[ + IsEqual, + IsEqual, + IsExtends, + ]> extends true + ? { input?: GenericInput } + : { input: GenericInput } + ) + & { + dependencies?: GenericDependencies; + } +>; + +export type ExecResult< + GenericFlow extends TheFlow, +> = GenericFlow extends TheFlow + ? InferredFunction extends TheFlowFunction< + any, + infer InferredGenerator + > + ? InferredGenerator extends TheFlowGenerator< + infer InferredOutput, + infer InferredEffect + > + ? [ + ( + | ( + InferredEffect extends Break + ? InferredValue + : never + ) + | InferredOutput + ), + Extract< + InferredEffect, + | Exit + | Injection + | Finalizer + | Step + >, + ] extends [ + infer InferredOutput, + infer InferredEffect, + ] + ? InferredGenerator extends AsyncGenerator + ? AsyncGenerator + : Generator + : never + : never + : never + : never; + +/** + * {@include flow/exec/index.md} + */ +export function exec< + GenericFlow extends( + | TheFlowFunction + | TheFlow + | TheFlowGenerator + ), + GenericWrapFlow extends WrapFlow, + const GenericParams extends ComputeExecParams< + FlowInput, + FlowDependencies + >, +>( + theFlow: GenericFlow, + ...[params]: ( + {} extends GenericParams + ? [params?: GenericParams] + : [params: GenericParams] + ) +): ExecResult> { + let result: undefined | IteratorResult = undefined; + let deferFunctions: (() => unknown)[] | undefined = undefined; + + const generator = justExec(() => { + if (Symbol.asyncIterator in theFlow || Symbol.iterator in theFlow) { + return forward(theFlow); + } else if (typeof theFlow === "function") { + return theFlow(params?.input); + } else { + return theFlowKind.getValue(theFlow).run(params?.input); + } + }); + + if (Symbol.asyncIterator in generator) { + return (async function *() { + try { + do { + result = await generator.next(); + if (result.done === true) { + break; + } else if (breakKind.has(result.value)) { + result = await generator.return( + breakKind.getValue(result.value).value, + ); + break; + } else if (exitKind.has(result.value)) { + yield result.value; + } else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push( + deferKind.getValue(result.value), + ); + } else if (finalizerKind.has(result.value)) { + yield result.value; + } else if (stepKind.has(result.value)) { + yield result.value; + } else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if ( + !params?.dependencies + || !(dependenceName in params.dependencies) + ) { + yield result.value; + } else { + injectionProperties.inject( + params.dependencies[dependenceName], + ); + } + } + } while (true); + + return result.value; + } finally { + await generator.return(undefined); + if (deferFunctions) { + await Promise.all( + deferFunctions.map( + justExec, + ), + ); + } + } + })() as never; + } + + return (function *() { + try { + do { + result = generator.next(); + if (result.done === true) { + break; + } else if (breakKind.has(result.value)) { + result = generator.return( + breakKind.getValue(result.value).value, + ); + break; + } else if (exitKind.has(result.value)) { + yield result.value; + } else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push( + deferKind.getValue(result.value), + ); + } else if (finalizerKind.has(result.value)) { + yield result.value; + } else if (stepKind.has(result.value)) { + yield result.value; + } else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if ( + !params?.dependencies + || !(dependenceName in params.dependencies) + ) { + yield result.value; + } else { + injectionProperties.inject( + params.dependencies[dependenceName], + ); + } + } + } while (true); + + return result.value; + } finally { + generator.return(undefined); + if (deferFunctions) { + deferFunctions.map( + justExec, + ); + } + } + })() as never; +} diff --git a/scripts/flow/exitIf.ts b/scripts/flow/exitIf.ts new file mode 100644 index 000000000..074466a9a --- /dev/null +++ b/scripts/flow/exitIf.ts @@ -0,0 +1,34 @@ +import { type NeverCoalescing, type AnyFunction } from "@scripts/common"; +import { createExit, type Exit } from "./theFlow"; + +/** + * {@include flow/exitIf/index.md} + */ +export function exitIf< + GenericValue extends unknown, + GenericPredicate extends GenericValue, +>( + value: GenericValue, + thePredicate: (value: GenericValue) => value is GenericPredicate +): Generator< + Exit< + NeverCoalescing, GenericPredicate> + >, + Exclude +>; +export function exitIf< + GenericValue extends unknown, +>( + value: GenericValue, + thePredicate: (value: GenericValue) => boolean +): Generator< + Exit, + GenericValue +>; +export function *exitIf(value: unknown, thePredicate: AnyFunction): any { + if (thePredicate(value) === true) { + yield createExit(value); + } else { + return value; + } +} diff --git a/scripts/flow/finalizer.ts b/scripts/flow/finalizer.ts new file mode 100644 index 000000000..44149bfbc --- /dev/null +++ b/scripts/flow/finalizer.ts @@ -0,0 +1,19 @@ +import { createFinalizer, type Finalizer } from "./theFlow"; + +/** + * {@include flow/finalizer/index.md} + */ +export function *finalizer< + GenericOutput extends unknown, +>( + theFunction: () => GenericOutput, +): ( + | Generator, undefined> + | ( + GenericOutput extends Promise + ? AsyncGenerator, undefined> + : never + ) + ) { + yield createFinalizer(theFunction); +} diff --git a/scripts/flow/index.ts b/scripts/flow/index.ts new file mode 100644 index 000000000..228cc82ab --- /dev/null +++ b/scripts/flow/index.ts @@ -0,0 +1,13 @@ +export * from "./types"; + +export * from "./run"; +export * from "./theFlow"; +export * from "./breakIf"; +export * from "./defer"; +export * from "./exec"; +export * from "./exitIf"; +export * from "./finalizer"; +export * from "./inject"; +export * from "./step"; +export * from "./initializer"; +export * from "./kind"; diff --git a/scripts/flow/initializer.ts b/scripts/flow/initializer.ts new file mode 100644 index 000000000..dbf584c4c --- /dev/null +++ b/scripts/flow/initializer.ts @@ -0,0 +1,102 @@ +import { type AnyFunction, type IsExtends, type Or } from "@scripts/common"; +import { type RequireAtLeastOne } from "@scripts/object"; +import { createDefer, createFinalizer, type Defer, type Finalizer } from "./theFlow"; + +export interface CreateInitializerParams< + GenericOutput extends unknown = unknown, +> { + defer?(output: Awaited): unknown; + finalizer?(output: Awaited): unknown; +} + +export type Initializer< + GenericArgs extends unknown[], + GenericOutput extends unknown, + GenericParams extends CreateInitializerParams, +> = Extract< + (...args: GenericArgs) => ( + ( + | ( + GenericParams["finalizer"] extends AnyFunction + ? Finalizer> + : never + ) + | ( + GenericParams["defer"] extends AnyFunction + ? Defer> + : never + ) + ) extends infer InferredEffect + ? ( + | Generator< + InferredEffect, + Awaited + > + | ( + Or<[ + IsExtends>, + IsExtends>>, + IsExtends>>, + ]> extends true + ? AsyncGenerator< + InferredEffect, + Awaited + > + : never + ) + ) + : never + + ), + any +>; + +/** + * {@include flow/createInitializer/index.md} + */ +export function createInitializer< + GenericArgs extends unknown[], + GenericOutput extends unknown, + GenericParams extends CreateInitializerParams, +>( + initializer: (...args: GenericArgs) => GenericOutput, + params: GenericParams & RequireAtLeastOne, +): Initializer< + GenericArgs, + GenericOutput, + GenericParams + > { + return (...args) => { + const result = initializer(...args); + const defer = params.defer; + const finalizer = params.finalizer; + + if (result instanceof Promise) { + return (async function *() { + const awaitedResult = await result; + + if (defer) { + yield createDefer(() => defer(awaitedResult)); + } + + if (finalizer) { + yield createFinalizer(() => finalizer(awaitedResult)); + } + + return awaitedResult; + })() as never; + } + + return (function *() { + if (defer) { + yield createDefer(() => defer(result as never)); + } + + if (finalizer) { + yield createFinalizer(() => finalizer(result as never)); + } + + return result; + })() as never; + }; +} diff --git a/scripts/flow/inject.ts b/scripts/flow/inject.ts new file mode 100644 index 000000000..dff90b592 --- /dev/null +++ b/scripts/flow/inject.ts @@ -0,0 +1,24 @@ +import { type DependenceHandler, type Injection, createInjection } from "./theFlow"; + +/** + * {@include flow/inject/index.md} + */ +export function *inject< + GenericDependenceHandler extends DependenceHandler, +>( + dependenceHandler: GenericDependenceHandler, +): Generator< + Injection, + ReturnType + > { + let dependence = undefined as ReturnType | undefined; + + yield createInjection({ + dependenceHandler, + inject: (value) => { + dependence = value; + }, + }); + + return dependence as never; +} diff --git a/scripts/flow/kind.ts b/scripts/flow/kind.ts new file mode 100644 index 000000000..b04654637 --- /dev/null +++ b/scripts/flow/kind.ts @@ -0,0 +1,6 @@ +import { createKindNamespace } from "@scripts/common"; + +export const createFlowKind = createKindNamespace( + // @ts-expect-error reserved kind namespace + "DuplojsUtilsFlow", +); diff --git a/scripts/flow/run.ts b/scripts/flow/run.ts new file mode 100644 index 000000000..ec377305c --- /dev/null +++ b/scripts/flow/run.ts @@ -0,0 +1,261 @@ +import { type SimplifyTopLevel, type IsEqual, type IsExtends, type Or, justExec, kindHeritage } from "@scripts/common"; +import { type TheFlow, type TheFlowFunction, type FlowInput, type WrapFlow, type TheFlowGenerator, type Exit, type Break, breakKind, exitKind, theFlowKind, stepKind, type Step, type FlowDependencies, injectionKind, type Effect, dependenceHandlerKind, type DependenceHandler, type ExtractFlowGenerator } from "./theFlow"; +import { deferKind } from "./theFlow/defer"; +import { finalizerKind } from "./theFlow/finalizer"; +import { createFlowKind } from "./kind"; + +type ComputeRunParams< + GenericInput extends unknown, + GenericDependencies extends Record, +> = SimplifyTopLevel< + & ( + Or<[ + IsEqual, + IsEqual, + IsExtends, + ]> extends true + ? { input?: GenericInput } + : { input: GenericInput } + ) + & { + includeDetails?: boolean; + } + & ( + {} extends GenericDependencies + ? { dependencies?: GenericDependencies } + : { dependencies: GenericDependencies } + ) +>; + +export interface FlowDetails< + GenericValue extends unknown, + GenericStepName extends string, +> { + result: GenericValue; + steps: GenericStepName[]; +} + +export type RunResult< + GenericFlow extends TheFlow, + GenericIncludeDetails extends boolean = false, +> = ( + GenericFlow extends TheFlow + ? InferredFunction extends TheFlowFunction< + any, + infer InferredGenerator + > + ? InferredGenerator extends TheFlowGenerator< + infer InferredOutput, + infer InferredEffect + > + ? ( + | ( + InferredEffect extends Exit + ? InferredValue + : InferredEffect extends Break + ? InferredValue + : never + ) + | InferredOutput + ) extends infer InferredResult + ? IsEqual extends true + ? FlowDetails< + InferredResult, + InferredEffect extends Step + ? InferredName + : never + > + : InferredResult + : never + : never + : never + : never +) extends infer InferredResult + ? ExtractFlowGenerator extends AsyncGenerator + ? Promise + : InferredResult + : never; + +export class MissingDependenceError extends kindHeritage( + "missing-dependence-error", + createFlowKind("missing-dependence-error"), + Error, +) { + public constructor( + public dependenceHandler: DependenceHandler, + ) { + super({}, [`Missing dependence : ${dependenceHandlerKind.getValue(dependenceHandler)}`]); + } +} + +/** + * {@include flow/run/index.md} + */ +export function run< + GenericFlow extends( + | TheFlowFunction + | TheFlow + ), + GenericWrapFlow extends WrapFlow, + const GenericParams extends ComputeRunParams< + FlowInput, + FlowDependencies + >, +>( + theFlow: GenericFlow, + ...[params]: ( + {} extends GenericParams + ? [params?: GenericParams] + : [params: GenericParams] + ) + +): RunResult< + GenericWrapFlow, + IsEqual + > { + let result: undefined | IteratorResult = undefined; + let deferFunctions: (() => unknown)[] | undefined = undefined; + let steps: string[] | undefined = undefined; + + const generator = typeof theFlow === "function" + ? theFlow(params?.input) + : theFlowKind.getValue(theFlow).run(params?.input); + + if (Symbol.asyncIterator in generator) { + return (async function() { + try { + do { + result = await generator.next(); + if (result.done === true) { + break; + } else if (breakKind.has(result.value)) { + result = await generator.return( + breakKind.getValue(result.value).value, + ); + break; + } else if (exitKind.has(result.value)) { + result = await generator.return( + exitKind.getValue(result.value).value, + ); + break; + } else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push( + deferKind.getValue(result.value), + ); + } else if (finalizerKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push( + finalizerKind.getValue(result.value), + ); + } else if ( + params?.includeDetails === true + && stepKind.has(result.value) + ) { + steps ??= []; + steps.push( + stepKind.getValue(result.value), + ); + } else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if ( + !params?.dependencies + || !(dependenceName in params.dependencies) + ) { + throw new MissingDependenceError(injectionProperties.dependenceHandler); + } + + injectionProperties.inject( + params.dependencies[dependenceName], + ); + } + } while (true); + + return params?.includeDetails === true + ? { + result: result.value, + steps: steps ?? [], + } + : result.value; + } finally { + await generator.return(undefined); + if (deferFunctions) { + await Promise.all( + deferFunctions.map( + justExec, + ), + ); + } + } + })() as never; + } + + try { + do { + result = generator.next(); + if (result.done === true) { + break; + } else if (breakKind.has(result.value)) { + result = generator.return( + breakKind.getValue(result.value).value, + ); + break; + } else if (exitKind.has(result.value)) { + result = generator.return( + exitKind.getValue(result.value).value, + ); + break; + } else if (deferKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push( + deferKind.getValue(result.value), + ); + } else if (finalizerKind.has(result.value)) { + deferFunctions ??= []; + deferFunctions.push( + finalizerKind.getValue(result.value), + ); + } else if ( + params?.includeDetails === true + && stepKind.has(result.value) + ) { + steps ??= []; + steps.push( + stepKind.getValue(result.value), + ); + } else if (injectionKind.has(result.value)) { + const injectionProperties = injectionKind.getValue(result.value); + + const dependenceName = dependenceHandlerKind.getValue(injectionProperties.dependenceHandler); + if ( + !params?.dependencies + || !(dependenceName in params.dependencies) + ) { + throw new MissingDependenceError(injectionProperties.dependenceHandler); + } + + injectionProperties.inject( + params.dependencies[dependenceName], + ); + } + } while (true); + + return ( + params?.includeDetails === true + ? { + result: result.value, + steps: steps ?? [], + } + : result.value + ) as never; + } finally { + generator.return(undefined); + if (deferFunctions) { + deferFunctions.map( + justExec, + ); + } + } +} diff --git a/scripts/flow/step.ts b/scripts/flow/step.ts new file mode 100644 index 000000000..aae8cb281 --- /dev/null +++ b/scripts/flow/step.ts @@ -0,0 +1,34 @@ +import { createStep, type Step } from "./theFlow"; + +/** + * {@include flow/step/index.md} + */ +export function step< + GenericName extends string, + GenericOutput extends unknown = void, +>( + name: GenericName, + theFunction?: () => GenericOutput, +): ( + GenericOutput extends Promise + ? AsyncGenerator< + Step, + Awaited + > + : Generator, GenericOutput> + ) { + const result = theFunction?.(); + + if (result instanceof Promise) { + return (async function *() { + yield createStep(name); + const awaitedResult = await result; + return awaitedResult; + })() as never; + } + + return (function *() { + yield createStep(name); + return result; + })() as never; +} diff --git a/scripts/flow/theFlow/break.ts b/scripts/flow/theFlow/break.ts new file mode 100644 index 000000000..bce58d708 --- /dev/null +++ b/scripts/flow/theFlow/break.ts @@ -0,0 +1,24 @@ +import { type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; + +export const breakKind = createFlowKind< + "break", + unknown +>("break"); + +export interface Break< + GenericValue extends unknown = unknown, +> extends Kind< + typeof breakKind.definition, + { value: GenericValue } + > { + +} + +export function createBreak< + GenericValue extends unknown = undefined, +>( + value: GenericValue = undefined as GenericValue, +): Break { + return breakKind.setTo({}, { value }); +} diff --git a/scripts/flow/theFlow/defer.ts b/scripts/flow/theFlow/defer.ts new file mode 100644 index 000000000..1a15ab31e --- /dev/null +++ b/scripts/flow/theFlow/defer.ts @@ -0,0 +1,24 @@ +import { type AnyFunction, type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; + +export const deferKind = createFlowKind< + "defer", + AnyFunction<[], unknown> +>("defer"); + +export interface Defer< + GenericValue extends unknown = unknown, +> extends Kind< + typeof deferKind.definition, + () => GenericValue + > { + +} + +export function createDefer< + GenericOutput extends unknown, +>( + value: () => GenericOutput, +): Defer { + return deferKind.setTo({}, value); +} diff --git a/scripts/flow/theFlow/dependence.ts b/scripts/flow/theFlow/dependence.ts new file mode 100644 index 000000000..610ea9f76 --- /dev/null +++ b/scripts/flow/theFlow/dependence.ts @@ -0,0 +1,54 @@ +import { type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; + +export const dependenceHandlerKind = createFlowKind< + "dependence-handler", + string +>("dependence-handler"); + +export interface DependenceHandlerKind< + GenericName extends string = string, +> extends Kind< + typeof dependenceHandlerKind.definition, + GenericName + > { + +} + +export type DependenceHandler< + GenericName extends string = string, + GenericType extends any = any, +> = ( + & DependenceHandlerKind + & ( + ( + implementation: GenericType + ) => GenericType + ) +); + +export type DependenceHandlerDefinition< + GenericName extends string = string, +> = ( + & DependenceHandlerKind + & ( + ( + implementation: GenericType + ) => GenericType + ) +); + +/** + * {@include flow/createDependence/index.md} + */ +export function createDependence< + GenericName extends string, +>( + name: GenericName, +): DependenceHandlerDefinition { + const dependenceHandler = function(implementation: any) { + return implementation; + }; + + return dependenceHandlerKind.setTo(dependenceHandler, name); +} diff --git a/scripts/flow/theFlow/exit.ts b/scripts/flow/theFlow/exit.ts new file mode 100644 index 000000000..c6bf04c1e --- /dev/null +++ b/scripts/flow/theFlow/exit.ts @@ -0,0 +1,24 @@ +import { type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; + +export const exitKind = createFlowKind< + "exit", + unknown +>("exit"); + +export interface Exit< + GenericValue extends unknown = unknown, +> extends Kind< + typeof exitKind.definition, + { value: GenericValue } + > { + +} + +export function createExit< + GenericValue extends unknown = undefined, +>( + value: GenericValue = undefined as GenericValue, +): Exit { + return exitKind.setTo({}, { value }); +} diff --git a/scripts/flow/theFlow/finalizer.ts b/scripts/flow/theFlow/finalizer.ts new file mode 100644 index 000000000..3328edd8a --- /dev/null +++ b/scripts/flow/theFlow/finalizer.ts @@ -0,0 +1,24 @@ +import { type AnyFunction, type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; + +export const finalizerKind = createFlowKind< + "finalizer", + AnyFunction<[], unknown> +>("finalizer"); + +export interface Finalizer< + GenericValue extends unknown = unknown, +> extends Kind< + typeof finalizerKind.definition, + () => GenericValue + > { + +} + +export function createFinalizer< + GenericOutput extends unknown, +>( + value: () => GenericOutput, +): Finalizer { + return finalizerKind.setTo({}, value); +} diff --git a/scripts/flow/theFlow/index.ts b/scripts/flow/theFlow/index.ts new file mode 100644 index 000000000..a2dbe5551 --- /dev/null +++ b/scripts/flow/theFlow/index.ts @@ -0,0 +1,133 @@ +import { type GetKindValue, type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; +import { type Step } from "./step"; +import { type Exit } from "./exit"; +import { type Injection } from "./injection"; +import { type Break } from "./break"; +import { type Defer } from "./defer"; +import { type Finalizer } from "./finalizer"; +import { type DependenceHandler, type dependenceHandlerKind } from "./dependence"; + +export * from "./step"; +export * from "./exit"; +export * from "./break"; +export * from "./injection"; +export * from "./defer"; +export * from "./finalizer"; +export * from "./dependence"; + +// <3 +export type Effect = ( + | Injection + | Step + | Exit + | Break + | Defer + | Finalizer +); + +export type TheFlowGenerator< + GenericOutput extends unknown = unknown, + GenericEffect extends Effect = Effect, +> = ( + | Generator< + GenericEffect, + GenericOutput + > + | AsyncGenerator< + GenericEffect, + GenericOutput + > +); + +export type TheFlowFunction< + GenericInput extends any = any, + GenericGenerator extends TheFlowGenerator = TheFlowGenerator, +> = (input: GenericInput) => GenericGenerator; + +export interface TheFlowProperties< + GenericFunction extends TheFlowFunction = TheFlowFunction, +> { + run: GenericFunction; +} + +export const theFlowKind = createFlowKind< + "the-flow", + TheFlowProperties +>("the-flow"); + +export interface TheFlow< + GenericFunction extends TheFlowFunction = TheFlowFunction, +> extends Kind< + typeof theFlowKind.definition, + TheFlowProperties< + GenericFunction + > + > { + +} + +/** + * {@include flow/create/index.md} + */ +export function create< + GenericTheFlowFunction extends TheFlowFunction, +>( + theFunction: GenericTheFlowFunction, +): TheFlow { + return theFlowKind.setTo( + {}, + { run: theFunction }, + ); +} + +export type FlowInput< + GenericFlow extends TheFlow, +> = GenericFlow extends TheFlow + ? InferredFunction extends TheFlowFunction + ? InferredInput + : never + : never; + +export type WrapFlow< + GenericFlow extends ( + | TheFlow + | TheFlowFunction + | TheFlowGenerator + ), +> = GenericFlow extends TheFlowGenerator + ? TheFlow> + : GenericFlow extends TheFlowFunction + ? TheFlow + : GenericFlow; + +export type FlowDependencies< + GenericFlow extends TheFlow, +> = ( + ExtractFlowGenerator extends TheFlowGenerator< + any, + infer InferredEffect + > + ? InferredEffect extends Injection + ? InferredDependenceHandler + : never + : never +) extends infer InferredDependenceHandler extends DependenceHandler + ? { + [ + Dependence in InferredDependenceHandler + as Extract, string> + ]: ReturnType + } + : never; + +export type ExtractFlowGenerator< + GenericFlow extends TheFlow, +> = GenericFlow extends TheFlow + ? InferredFunction extends TheFlowFunction< + any, + infer InferredGenerator + > + ? InferredGenerator + : never + : never; diff --git a/scripts/flow/theFlow/injection.ts b/scripts/flow/theFlow/injection.ts new file mode 100644 index 000000000..7b8039652 --- /dev/null +++ b/scripts/flow/theFlow/injection.ts @@ -0,0 +1,32 @@ +import { type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; +import { type DependenceHandler } from "./dependence"; + +export interface InjectionProperties< + GenericDependenceHandler extends DependenceHandler = DependenceHandler, +> { + dependenceHandler: GenericDependenceHandler; + inject(value: ReturnType): void; +} + +export const injectionKind = createFlowKind< + "injection", + InjectionProperties +>("injection"); + +export interface Injection< + GenericDependenceHandler extends DependenceHandler = DependenceHandler, +> extends Kind< + typeof injectionKind.definition, + InjectionProperties + > { + +} + +export function createInjection< + GenericDependenceHandler extends DependenceHandler = DependenceHandler, +>( + properties: InjectionProperties, +): Injection { + return injectionKind.setTo({}, properties); +} diff --git a/scripts/flow/theFlow/step.ts b/scripts/flow/theFlow/step.ts new file mode 100644 index 000000000..6a778d6a2 --- /dev/null +++ b/scripts/flow/theFlow/step.ts @@ -0,0 +1,25 @@ +import { type Kind } from "@scripts/common"; +import { createFlowKind } from "../kind"; + +export const stepKind = createFlowKind< + "step", + string +>("step"); + +export interface Step< + GenericName extends string = string, +> extends Kind< + typeof stepKind.definition, + GenericName + > { + +} + +export function createStep< + GenericName extends string, +>( + name: GenericName, +): Step { + return stepKind.setTo({}, name); +} + diff --git a/scripts/flow/types/index.ts b/scripts/flow/types/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/index.ts b/scripts/index.ts index b92d8e96d..1cf65ec50 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -35,3 +35,6 @@ export * as DDate from "./date"; export * as C from "./clean"; export * as DClean from "./clean"; + +export * as F from "./flow"; +export * as DFlow from "./flow"; diff --git a/scripts/object/types/requireAtLeastOne.ts b/scripts/object/types/requireAtLeastOne.ts index 08938f5eb..1c5dbc464 100644 --- a/scripts/object/types/requireAtLeastOne.ts +++ b/scripts/object/types/requireAtLeastOne.ts @@ -1,6 +1,7 @@ import { type IsEqual } from "@scripts/common"; declare const SymbolRequireAtLeastOneError: unique symbol; +declare const SymbolOneOf: unique symbol; export type RequireAtLeastOne< GenericObject extends object, @@ -8,6 +9,6 @@ export type RequireAtLeastOne< > = IsEqual, never> extends true ? { [SymbolRequireAtLeastOneError]: "requires at least one key."; - oneOf: GenericKeys; + [SymbolOneOf]: `key: ${Extract}`; } : unknown; diff --git a/tests/common/kind.test.ts b/tests/common/kind.test.ts index 876499cde..1865b859c 100644 --- a/tests/common/kind.test.ts +++ b/tests/common/kind.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable prefer-arrow-callback */ import { createKind, keyKindPrefix, type KindDefinition, type Kind, type ExpectType, kindHeritage, type SimplifyTopLevel, createKindNamespace, isRuntimeKind } from "@scripts"; describe("theKind", () => { @@ -61,6 +62,8 @@ describe("theKind", () => { expect(predicate).toBe(true); + expect(myKind.has(myKind.setTo(function() {}))).toBe(true); + if (predicate) { type Check = ExpectType< typeof newObject, diff --git a/tests/flow/breakIf.test.ts b/tests/flow/breakIf.test.ts new file mode 100644 index 000000000..bb83e8353 --- /dev/null +++ b/tests/flow/breakIf.test.ts @@ -0,0 +1,55 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("breakIf", () => { + it("yields a break effect when the predicate returns true", () => { + const result = DFlow.breakIf("value", (input) => input === "value"); + + expect(result.next()).toStrictEqual({ + done: false, + value: DFlow.createBreak("value"), + }); + expect(result.next()).toStrictEqual({ + done: true, + value: undefined, + }); + + type check = ExpectType< + typeof result, + Generator, string, any>, + "strict" + >; + }); + + it("returns the input when the predicate returns false", () => { + const result = DFlow.breakIf("test" as "value" | "test", (input) => input === "value"); + + expect(result.next()).toStrictEqual({ + done: true, + value: "test", + }); + + type check = ExpectType< + typeof result, + Generator, "test", any>, + "strict" + >; + }); + + it("supports not predicate", () => { + const result = DFlow.breakIf( + 10, + (input) => input > 20, + ); + + expect(result.next()).toStrictEqual({ + done: true, + value: 10, + }); + + type check = ExpectType< + typeof result, + Generator, 10, any>, + "strict" + >; + }); +}); diff --git a/tests/flow/defer.test.ts b/tests/flow/defer.test.ts new file mode 100644 index 000000000..0b36ee3c7 --- /dev/null +++ b/tests/flow/defer.test.ts @@ -0,0 +1,25 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("defer", () => { + it("yields a defer effect with the provided function", () => { + const theFunction = () => "value"; + const result = DFlow.defer(theFunction); + const firstResult = result.next(); + + expect(firstResult).toStrictEqual({ + done: false, + value: DFlow.createDefer(theFunction), + }); + expect(DFlow.deferKind.getValue(firstResult.value!)).toBe(theFunction); + expect(result.next()).toStrictEqual({ + done: true, + value: undefined, + }); + + type check = ExpectType< + typeof result, + Generator, undefined, any>, + "strict" + >; + }); +}); diff --git a/tests/flow/exec.test.ts b/tests/flow/exec.test.ts new file mode 100644 index 000000000..ee3f487cc --- /dev/null +++ b/tests/flow/exec.test.ts @@ -0,0 +1,791 @@ +/* eslint-disable require-yield */ +import { DFlow, type ExpectType } from "@scripts"; + +describe("exec", () => { + it("passe input", () => { + const result = DFlow.run( + function *(input: string) { + const result = yield *DFlow.exec( + function *(subInput: string) { + return subInput; + }, + { input }, + ); + + return result; + }, + { input: "test" }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toBe("test"); + }); + + it("run flow", () => { + const flow = DFlow.create( + function *(subInput: string) { + return subInput; + }, + ); + const result = DFlow.run( + function *(input: string) { + const result = yield *DFlow.exec( + flow, + { input }, + ); + + return result; + }, + { input: "test" }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toBe("test"); + }); + + it("run generator", () => { + const flow = function *(subInput: T) { + return subInput; + }; + const result = DFlow.run( + function *(input: string) { + const result = yield *DFlow.exec( + flow(input), + ); + + return result; + }, + { input: "test" }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toBe("test"); + }); + + describe("sync", () => { + const spyDefer = vi.fn((...args: any[]) => {}); + const spyFinalizer = vi.fn((...args: any[]) => {}); + + afterEach(() => { + spyDefer.mockClear(); + spyFinalizer.mockClear(); + }); + + it("return string", () => { + const result = DFlow.run( + function *() { + const result = yield *DFlow.exec( + function *() { + return "test"; + }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + return result; + }, + ); + + expect(result).toBe("test"); + }); + + it("break and return number and active defer", () => { + const result = DFlow.run( + function *() { + yield *DFlow.defer(() => void spyDefer("run")); + + const result = yield *DFlow.exec( + function *() { + yield *DFlow.defer(() => void spyDefer("exec")); + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return "test"; + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + return result; + }, + ); + + expect(result).toBe(2); + expect(spyDefer).toHaveBeenNthCalledWith(1, "exec"); + expect(spyDefer).toHaveBeenNthCalledWith(2, "run"); + }); + + it("break and return number trigger finally block", () => { + const spyFinally = vi.fn(); + const result = DFlow.run( + function *() { + const result = yield *DFlow.exec( + function *() { + try { + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + return "test"; + } finally { + spyFinally(); + } + }, + ); + + return result; + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + expect(result).toBe(2); + expect(spyFinally).toHaveBeenCalledOnce(); + }); + + it("exit and return number and active finalizer", () => { + const result = DFlow.run( + function *() { + yield *DFlow.finalizer(() => void spyFinalizer("run")); + + const result = yield *DFlow.exec( + function *() { + yield *DFlow.finalizer(() => void spyFinalizer("exec")); + yield *DFlow.defer(spyDefer); + const value = yield *DFlow.exitIf(2, (value) => value === 2); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return "test"; + }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + return result; + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + expect(result).toBe(2); + expect(spyFinalizer).toHaveBeenCalledTimes(2); + expect(spyFinalizer).toHaveBeenNthCalledWith(1, "run"); + expect(spyFinalizer).toHaveBeenNthCalledWith(2, "exec"); + expect(spyDefer).toHaveBeenCalledOnce(); + }); + + it("deep exit trigger all finally block", () => { + const spyFinally = vi.fn(); + const result = DFlow.run( + function *() { + yield *DFlow.defer(() => void spyDefer("three")); + yield *DFlow.finalizer(() => void spyFinalizer("three")); + + try { + const result = yield *DFlow.exec( + function *() { + yield *DFlow.defer(() => void spyDefer("two")); + yield *DFlow.finalizer(() => void spyFinalizer("two")); + + try { + const result = yield *DFlow.exec( + function *() { + yield *DFlow.defer(() => void spyDefer("one")); + yield *DFlow.finalizer(() => void spyFinalizer("one")); + + try { + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + return "test"; + } finally { + spyFinally("one"); + } + }, + ); + + return result; + } finally { + spyFinally("two"); + } + }, + ); + + return result; + } finally { + spyFinally("three"); + } + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + expect(result).toBe(2); + + expect(spyFinalizer).toHaveBeenCalledTimes(3); + expect(spyFinalizer).toHaveBeenNthCalledWith(1, "three"); + expect(spyFinalizer).toHaveBeenNthCalledWith(2, "two"); + expect(spyFinalizer).toHaveBeenNthCalledWith(3, "one"); + + expect(spyDefer).toHaveBeenCalledTimes(3); + expect(spyDefer).toHaveBeenNthCalledWith(1, "one"); + expect(spyDefer).toHaveBeenNthCalledWith(2, "two"); + expect(spyDefer).toHaveBeenNthCalledWith(3, "three"); + + expect(spyFinally).toHaveBeenCalledTimes(3); + expect(spyFinally).toHaveBeenNthCalledWith(1, "one"); + expect(spyFinally).toHaveBeenNthCalledWith(2, "two"); + expect(spyFinally).toHaveBeenNthCalledWith(3, "three"); + }); + + it("pass on first step and passe on exit", () => { + const result = DFlow.run( + function *() { + yield *DFlow.exec( + function *() { + yield *DFlow.step("here 1"); + yield *DFlow.exitIf(2, (value) => value === 2); + yield *DFlow.step("here 2"); + }, + ); + + return "test"; + }, + { includeDetails: true }, + ); + + type check = ExpectType< + typeof result, + DFlow.FlowDetails, + "strict" + >; + + expect(result).toStrictEqual({ + result: 2, + steps: ["here 1"], + }); + }); + + it("inject dependence", () => { + const superDependence = DFlow.createDependence("test"); + const result = DFlow.run( + function *() { + const result = yield *DFlow.exec( + function *() { + const myDep = yield *DFlow.inject(superDependence); + + return myDep; + }, + ); + + return result; + }, + { dependencies: { test: "superDep" } }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toStrictEqual("superDep"); + }); + + it("override inject dependence", () => { + const superDependence = DFlow.createDependence("test"); + const result = DFlow.run( + function *() { + const result = yield *DFlow.exec( + function *() { + const myDep = yield *DFlow.inject(superDependence); + + return myDep; + }, + { dependencies: { test: "overrideSuperDep" } }, + ); + + return result; + }, + { dependencies: { test: "superDep" } }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toStrictEqual("overrideSuperDep"); + }); + + it("inject missing dependence", () => { + const superDependence = DFlow.createDependence("test"); + + expect(() => DFlow.run( + function *() { + const result = yield *DFlow.exec( + function *() { + const myDep = yield *DFlow.inject(superDependence); + + return myDep; + }, + { dependencies: { } as never }, + ); + + return result; + }, + { dependencies: { } as never }, + )).toThrowError(DFlow.MissingDependenceError); + }); + + it("yield not support value", () => { + const value = DFlow.run( + function *() { + // @ts-expect-error unexpect yield value + const result = yield *DFlow.exec( + // @ts-expect-error unexpect yield value + function *() { + yield 5; + + return "test"; + }, + ); + + return result; + }, + ); + + expect(value).toBe("test"); + }); + }); + + describe("async", () => { + const spyDefer = vi.fn(async(...args: any[]) => Promise.resolve()); + const spyFinalizer = vi.fn(async(...args: any[]) => Promise.resolve()); + + afterEach(() => { + spyDefer.mockClear(); + spyFinalizer.mockClear(); + }); + + it("return string", async() => { + const result = DFlow.run( + async function *() { + const result = yield *DFlow.exec( + async function *() { + await Promise.resolve(); + return "test"; + }, + ); + + await Promise.resolve(); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + return result; + }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + await expect(result).resolves.toBe("test"); + }); + + it("break and return number and active defer", async() => { + const result = DFlow.run( + async function *() { + yield *DFlow.defer(() => spyDefer("run")); + + const result = yield *DFlow.exec( + async function *() { + yield *DFlow.defer(() => spyDefer("exec")); + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + await Promise.resolve(); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return "test"; + }, + ); + + await Promise.resolve(); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + return result; + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + expect(spyDefer).toHaveBeenNthCalledWith(1, "exec"); + expect(spyDefer).toHaveBeenNthCalledWith(2, "run"); + }); + + it("break and return number trigger finally block", async() => { + const spyFinally = vi.fn(); + const result = DFlow.run( + async function *() { + const result = yield *DFlow.exec( + async function *() { + try { + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + await Promise.resolve(); + + return "test"; + } finally { + spyFinally(); + } + }, + ); + + await Promise.resolve(); + + return result; + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + expect(spyFinally).toHaveBeenCalledOnce(); + }); + + it("exit and return number and active finalizer", async() => { + const result = DFlow.run( + async function *() { + yield *DFlow.finalizer(() => spyFinalizer("run")); + + const result = yield *DFlow.exec( + async function *() { + yield *DFlow.finalizer(() => spyFinalizer("exec")); + yield *DFlow.defer(spyDefer); + const value = yield *DFlow.exitIf(2, (value) => value === 2); + + await Promise.resolve(); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return "test"; + }, + ); + + await Promise.resolve(); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + return result; + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + expect(spyFinalizer).toHaveBeenCalledTimes(2); + expect(spyFinalizer).toHaveBeenNthCalledWith(1, "run"); + expect(spyFinalizer).toHaveBeenNthCalledWith(2, "exec"); + expect(spyDefer).toHaveBeenCalledOnce(); + }); + + it("deep exit trigger all finally block", async() => { + const spyFinally = vi.fn(); + const result = DFlow.run( + async function *() { + yield *DFlow.defer(() => spyDefer("three")); + yield *DFlow.finalizer(() => spyFinalizer("three")); + + try { + const result = yield *DFlow.exec( + async function *() { + yield *DFlow.defer(() => spyDefer("two")); + yield *DFlow.finalizer(() => spyFinalizer("two")); + + try { + const result = yield *DFlow.exec( + async function *() { + yield *DFlow.defer(() => spyDefer("one")); + yield *DFlow.finalizer(() => spyFinalizer("one")); + + await Promise.resolve(); + + try { + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + return "test"; + } finally { + spyFinally("one"); + } + }, + ); + + await Promise.resolve(); + + return result; + } finally { + spyFinally("two"); + } + }, + ); + + await Promise.resolve(); + + return result; + } finally { + spyFinally("three"); + } + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + + expect(spyFinalizer).toHaveBeenCalledTimes(3); + expect(spyFinalizer).toHaveBeenNthCalledWith(1, "three"); + expect(spyFinalizer).toHaveBeenNthCalledWith(2, "two"); + expect(spyFinalizer).toHaveBeenNthCalledWith(3, "one"); + + expect(spyDefer).toHaveBeenCalledTimes(3); + expect(spyDefer).toHaveBeenNthCalledWith(1, "one"); + expect(spyDefer).toHaveBeenNthCalledWith(2, "two"); + expect(spyDefer).toHaveBeenNthCalledWith(3, "three"); + + expect(spyFinally).toHaveBeenCalledTimes(3); + expect(spyFinally).toHaveBeenNthCalledWith(1, "one"); + expect(spyFinally).toHaveBeenNthCalledWith(2, "two"); + expect(spyFinally).toHaveBeenNthCalledWith(3, "three"); + }); + + it("pass on first step and passe on exit", async() => { + const result = DFlow.run( + async function *() { + yield *DFlow.exec( + async function *() { + yield *DFlow.step("here 1"); + yield *DFlow.exitIf(2, (value) => value === 2); + yield *DFlow.step("here 2"); + + await Promise.resolve(); + }, + ); + + await Promise.resolve(); + + return "test"; + }, + { includeDetails: true }, + ); + + type check = ExpectType< + typeof result, + Promise>, + "strict" + >; + + await expect(result).resolves.toStrictEqual({ + result: 2, + steps: ["here 1"], + }); + }); + + it("inject dependence", async() => { + const superDependence = DFlow.createDependence("test"); + const result = DFlow.run( + async function *() { + const result = yield *DFlow.exec( + async function *() { + const myDep = yield *DFlow.inject(superDependence); + + await Promise.resolve(); + + return myDep; + }, + ); + + await Promise.resolve(); + + return result; + }, + { dependencies: { test: "superDep" } }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + await expect(result).resolves.toStrictEqual("superDep"); + }); + + it("override inject dependence", async() => { + const superDependence = DFlow.createDependence("test"); + const result = DFlow.run( + async function *() { + const result = yield *DFlow.exec( + async function *() { + const myDep = yield *DFlow.inject(superDependence); + + await Promise.resolve(); + + return myDep; + }, + { dependencies: { test: "overrideSuperDep" } }, + ); + + await Promise.resolve(); + + return result; + }, + { dependencies: { test: "superDep" } }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + await expect(result).resolves.toStrictEqual("overrideSuperDep"); + }); + + it("inject missing dependence", async() => { + const superDependence = DFlow.createDependence("test"); + + const result = DFlow.run( + async function *() { + const result = yield *DFlow.exec( + async function *() { + const myDep = yield *DFlow.inject(superDependence); + + await Promise.resolve(); + + return myDep; + }, + { dependencies: { } as never }, + ); + + await Promise.resolve(); + + return result; + }, + { dependencies: { } as never }, + ); + + await expect(result).rejects.toThrowError(DFlow.MissingDependenceError); + }); + + it("yield not support value", async() => { + const value = DFlow.run( + async function *() { + const result = yield *DFlow.exec( + // @ts-expect-error unexpect yield value + async function *() { + yield 5; + + await Promise.resolve(); + + return "test"; + }, + ); + + await Promise.resolve(); + + return result; + }, + ); + + await expect(value).resolves.toBe("test"); + }); + }); +}); diff --git a/tests/flow/exitIf.test.ts b/tests/flow/exitIf.test.ts new file mode 100644 index 000000000..63bfdf530 --- /dev/null +++ b/tests/flow/exitIf.test.ts @@ -0,0 +1,55 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("exitIf", () => { + it("yields a exit effect when the predicate returns true", () => { + const result = DFlow.exitIf("value", (input) => input === "value"); + + expect(result.next()).toStrictEqual({ + done: false, + value: DFlow.createExit("value"), + }); + expect(result.next()).toStrictEqual({ + done: true, + value: undefined, + }); + + type check = ExpectType< + typeof result, + Generator, string, any>, + "strict" + >; + }); + + it("returns the input when the predicate returns false", () => { + const result = DFlow.exitIf("test" as "value" | "test", (input) => input === "value"); + + expect(result.next()).toStrictEqual({ + done: true, + value: "test", + }); + + type check = ExpectType< + typeof result, + Generator, "test", any>, + "strict" + >; + }); + + it("supports not predicate", () => { + const result = DFlow.exitIf( + 10, + (input) => input > 20, + ); + + expect(result.next()).toStrictEqual({ + done: true, + value: 10, + }); + + type check = ExpectType< + typeof result, + Generator, 10, any>, + "strict" + >; + }); +}); diff --git a/tests/flow/finalizer.test.ts b/tests/flow/finalizer.test.ts new file mode 100644 index 000000000..d5c458ed1 --- /dev/null +++ b/tests/flow/finalizer.test.ts @@ -0,0 +1,25 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("finalizer", () => { + it("yields a finalizer effect with the provided function", () => { + const theFunction = () => "value"; + const result = DFlow.finalizer(theFunction); + const firstResult = result.next(); + + expect(firstResult).toStrictEqual({ + done: false, + value: DFlow.createFinalizer(theFunction), + }); + expect(DFlow.finalizerKind.getValue(firstResult.value!)).toBe(theFunction); + expect(result.next()).toStrictEqual({ + done: true, + value: undefined, + }); + + type check = ExpectType< + typeof result, + Generator, undefined, any>, + "strict" + >; + }); +}); diff --git a/tests/flow/initializer.test.ts b/tests/flow/initializer.test.ts new file mode 100644 index 000000000..30be4e868 --- /dev/null +++ b/tests/flow/initializer.test.ts @@ -0,0 +1,151 @@ + +import { DFlow, type ExpectType } from "@scripts"; + +describe("createInitializer", () => { + describe("sync", () => { + it("returns the initializer result and runs defer with the returned value", () => { + const spyDefer = vi.fn((output: string) => output.length); + const initializer = DFlow.createInitializer( + (input: string) => `hello ${input}`, + { defer: spyDefer }, + ); + const result = DFlow.run( + function *() { + const value = yield *initializer("world"); + + type check = ExpectType< + typeof value, + string, + "strict" + >; + + return value; + }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + type checkInitializer = ExpectType< + typeof initializer, + (input: string) => Generator, string, any>, + "strict" + >; + + expect(result).toBe("hello world"); + expect(spyDefer).toHaveBeenCalledOnce(); + expect(spyDefer).toHaveBeenCalledWith("hello world"); + }); + + it("returns the initializer result and runs finalizer with the returned value", () => { + const spyFinalizer = vi.fn((output: number) => output.toString()); + const initializer = DFlow.createInitializer( + (left: number, right: number) => left + right, + { finalizer: spyFinalizer }, + ); + const result = DFlow.run( + function *() { + const value = yield *initializer(20, 22); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return value; + }, + ); + + type check = ExpectType< + typeof result, + number, + "strict" + >; + + expect(result).toBe(42); + expect(spyFinalizer).toHaveBeenCalledOnce(); + expect(spyFinalizer).toHaveBeenCalledWith(42); + }); + }); + + describe("async", () => { + it("returns the awaited initializer result and runs defer with the awaited value", async() => { + const spyDefer = vi.fn(async(output: string) => Promise.resolve(output.length)); + const initializer = DFlow.createInitializer( + async(input: string) => Promise.resolve(`hello ${input}`), + { defer: spyDefer }, + ); + const result = DFlow.run( + async function *() { + const value = yield *initializer("world"); + + await Promise.resolve(); + + type check = ExpectType< + typeof value, + string, + "strict" + >; + + return value; + }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + await expect(result).resolves.toBe("hello world"); + expect(spyDefer).toHaveBeenCalledOnce(); + expect(spyDefer).toHaveBeenCalledWith("hello world"); + }); + + it("returns the awaited initializer result and runs finalizer with the awaited value", async() => { + const spyFinalizer = vi.fn(async(output: number) => Promise.resolve(output.toString())); + const initializer = DFlow.createInitializer( + async(left: number, right: number) => Promise.resolve(left + right), + { finalizer: spyFinalizer }, + ); + const result = DFlow.run( + async function *() { + const value = yield *initializer(20, 22); + + await Promise.resolve(); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return value; + }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + type checkInitializer = ExpectType< + typeof initializer, + (left: number, right: number) => ( + | Generator>, number, any> + | AsyncGenerator>, number, any> + ), + "strict" + >; + + await expect(result).resolves.toBe(42); + expect(spyFinalizer).toHaveBeenCalledOnce(); + expect(spyFinalizer).toHaveBeenCalledWith(42); + }); + }); +}); diff --git a/tests/flow/inject.test.ts b/tests/flow/inject.test.ts new file mode 100644 index 000000000..3994934dd --- /dev/null +++ b/tests/flow/inject.test.ts @@ -0,0 +1,30 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("inject", () => { + it("yields an injection effect and returns the injected dependence", () => { + const dependenceHandler = DFlow.createDependence("service"); + const result = DFlow.inject(dependenceHandler); + const firstResult = result.next(); + + expect(firstResult).toStrictEqual({ + done: false, + value: DFlow.createInjection({ + dependenceHandler, + inject: expect.any(Function), + }), + }); + + DFlow.injectionKind.getValue(firstResult.value as DFlow.Injection).inject("service"); + + expect(result.next()).toStrictEqual({ + done: true, + value: "service", + }); + + type check = ExpectType< + typeof result, + Generator, string, any>, + "strict" + >; + }); +}); diff --git a/tests/flow/run.test.ts b/tests/flow/run.test.ts new file mode 100644 index 000000000..8298ffcc6 --- /dev/null +++ b/tests/flow/run.test.ts @@ -0,0 +1,425 @@ +/* eslint-disable require-yield */ +import { DFlow, type ExpectType } from "@scripts"; + +describe("run", () => { + it("passe input", () => { + const result = DFlow.run( + function *(input: string) { + return input; + }, + { input: "test" }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toBe("test"); + }); + + it("run flow", () => { + const flow = DFlow.create( + function *(input: string) { + return input; + }, + ); + const result = DFlow.run( + flow, + { input: "test" }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toBe("test"); + }); + + describe("sync", () => { + const spyDefer = vi.fn(() => {}); + const spyFinalizer = vi.fn(() => {}); + + afterEach(() => { + spyDefer.mockClear(); + spyFinalizer.mockClear(); + }); + + it("return string", () => { + const result = DFlow.run( + function *() { + return "test"; + }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toBe("test"); + }); + + it("break and return number and active defer", () => { + const result = DFlow.run( + function *() { + yield *DFlow.defer(spyDefer); + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return "test"; + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + expect(result).toBe(2); + expect(spyDefer).toHaveBeenCalledOnce(); + }); + + it("break and return number trigger finally block", () => { + const spyFinally = vi.fn(); + const result = DFlow.run( + function *() { + try { + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + return "test"; + } finally { + spyFinally(); + } + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + expect(result).toBe(2); + expect(spyFinally).toHaveBeenCalledOnce(); + }); + + it("exit and return number and active finalizer", () => { + const result = DFlow.run( + function *() { + yield *DFlow.finalizer(spyFinalizer); + const value = yield *DFlow.exitIf(2, (value) => value === 2); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return "test"; + }, + ); + + type check = ExpectType< + typeof result, + 2 | string, + "strict" + >; + + expect(result).toBe(2); + expect(spyFinalizer).toHaveBeenCalledOnce(); + }); + + it("pass on first step and passe on break", () => { + const result = DFlow.run( + function *() { + yield *DFlow.step("here 1"); + yield *DFlow.breakIf(2, (value) => value === 2); + yield *DFlow.step("here 2"); + + return "test"; + }, + { includeDetails: true }, + ); + + type check = ExpectType< + typeof result, + DFlow.FlowDetails, + "strict" + >; + + expect(result).toStrictEqual({ + result: 2, + steps: ["here 1"], + }); + }); + + it("include details without step", () => { + const result = DFlow.run( + function *() { + return "test"; + }, + { includeDetails: true }, + ); + + type check = ExpectType< + typeof result, + DFlow.FlowDetails, + "strict" + >; + + expect(result).toStrictEqual({ + result: "test", + steps: [], + }); + }); + + it("inject dependence", () => { + const superDependence = DFlow.createDependence("test"); + const result = DFlow.run( + function *() { + const myDep = yield *DFlow.inject(superDependence); + + return myDep; + }, + { dependencies: { test: "superDep" } }, + ); + + type check = ExpectType< + typeof result, + string, + "strict" + >; + + expect(result).toStrictEqual("superDep"); + }); + + it("inject missing dependence", () => { + const superDependence = DFlow.createDependence("test"); + + expect(() => DFlow.run( + function *() { + const myDep = yield *DFlow.inject(superDependence); + + return myDep; + }, + { dependencies: { } as never }, + )).toThrowError(DFlow.MissingDependenceError); + }); + + it("yield not support value", () => { + const value = DFlow.run( + // @ts-expect-error unexpect yield value + function *() { + yield 5; + + return "test"; + }, + ); + + expect(value).toBe("test"); + }); + }); + + describe("async", () => { + const spyDefer = vi.fn(() => Promise.resolve()); + const spyFinalizer = vi.fn(() => Promise.resolve()); + + afterEach(() => { + spyDefer.mockClear(); + spyFinalizer.mockClear(); + }); + + it("return string", async() => { + const result = DFlow.run( + async function *() { + return Promise.resolve("test"); + }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + await expect(result).resolves.toBe("test"); + }); + + it("break and return number and active defer", async() => { + const result = DFlow.run( + async function *() { + yield *DFlow.defer(spyDefer); + const value = yield *DFlow.breakIf(2, (value) => value === 2); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return Promise.resolve("test"); + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + expect(spyDefer).toHaveBeenCalledOnce(); + }); + + it("exit and return number and active finalizer", async() => { + const result = DFlow.run( + async function *() { + yield *DFlow.finalizer(spyFinalizer); + const value = yield *DFlow.exitIf(2, (value) => value === 2); + + type check = ExpectType< + typeof value, + number, + "strict" + >; + + return Promise.resolve("test"); + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + expect(spyFinalizer).toHaveBeenCalledOnce(); + }); + + it("break and return number trigger finally block", async() => { + const spyFinally = vi.fn(); + const result = DFlow.run( + async function *() { + try { + const value = yield *DFlow.breakIf(2, (value) => value === 2); + await Promise.resolve(); + return "test"; + } finally { + spyFinally(); + } + }, + ); + + type check = ExpectType< + typeof result, + Promise<2 | string>, + "strict" + >; + + await expect(result).resolves.toBe(2); + expect(spyFinally).toHaveBeenCalledOnce(); + }); + + it("pass on first step and passe on break", async() => { + const result = DFlow.run( + async function *() { + yield *DFlow.step("here 1"); + yield *DFlow.breakIf(2, (value) => value === 2); + yield *DFlow.step("here 2"); + + return Promise.resolve("test"); + }, + { includeDetails: true }, + ); + + type check = ExpectType< + typeof result, + Promise>, + "strict" + >; + + await expect(result).resolves.toStrictEqual({ + result: 2, + steps: ["here 1"], + }); + }); + + it("include details without step", async() => { + const result = DFlow.run( + async function *() { + return Promise.resolve("test"); + }, + { includeDetails: true }, + ); + + type check = ExpectType< + typeof result, + Promise>, + "strict" + >; + + await expect(result).resolves.toStrictEqual({ + result: "test", + steps: [], + }); + }); + + it("inject dependence", async() => { + const superDependence = DFlow.createDependence("test"); + const result = DFlow.run( + async function *() { + const myDep = yield *DFlow.inject(superDependence); + + return Promise.resolve(myDep); + }, + { dependencies: { test: "superDep" } }, + ); + + type check = ExpectType< + typeof result, + Promise, + "strict" + >; + + await expect(result).resolves.toStrictEqual("superDep"); + }); + + it("inject missing dependence", async() => { + const superDependence = DFlow.createDependence("test"); + + await expect(() => DFlow.run( + async function *() { + const myDep = yield *DFlow.inject(superDependence); + + return Promise.resolve(myDep); + }, + { dependencies: { } as never }, + )).rejects.toThrowError(DFlow.MissingDependenceError); + }); + + it("yield not support value", async() => { + const value = DFlow.run( + // @ts-expect-error unexpect yield value + async function *() { + yield 5; + + return Promise.resolve("test"); + }, + ); + + await expect(value).resolves.toBe("test"); + }); + }); +}); diff --git a/tests/flow/step.test.ts b/tests/flow/step.test.ts new file mode 100644 index 000000000..459bea408 --- /dev/null +++ b/tests/flow/step.test.ts @@ -0,0 +1,60 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("step", () => { + it("yields a step effect and returns the sync function result", () => { + const result = DFlow.step("my-step", () => "value"); + + expect(result.next()).toStrictEqual({ + done: false, + value: DFlow.createStep("my-step"), + }); + expect(result.next()).toStrictEqual({ + done: true, + value: "value", + }); + + type check = ExpectType< + typeof result, + Generator, string, any>, + "strict" + >; + }); + + it("yields a step effect and awaits the async function result", async() => { + const result = DFlow.step("my-step", async() => Promise.resolve("value")); + + expect(await result.next()).toStrictEqual({ + done: false, + value: DFlow.createStep("my-step"), + }); + expect(await result.next()).toStrictEqual({ + done: true, + value: "value", + }); + + type check = ExpectType< + typeof result, + AsyncGenerator, string, any>, + "strict" + >; + }); + + it("returns undefined when no function is provided", () => { + const result = DFlow.step("my-step"); + + expect(result.next()).toStrictEqual({ + done: false, + value: DFlow.createStep("my-step"), + }); + expect(result.next()).toStrictEqual({ + done: true, + value: undefined, + }); + + type check = ExpectType< + typeof result, + Generator, void, any>, + "strict" + >; + }); +}); diff --git a/tests/flow/theflow/break.test.ts b/tests/flow/theflow/break.test.ts new file mode 100644 index 000000000..12f9074a5 --- /dev/null +++ b/tests/flow/theflow/break.test.ts @@ -0,0 +1,24 @@ + +import { DFlow, type ExpectType } from "@scripts"; + +describe("createBreak", () => { + it("creates a break kind with the provided value", () => { + const result = DFlow.createBreak("stop" as const); + + expect(DFlow.breakKind.has(result)).toBe(true); + expect(DFlow.breakKind.getValue(result)).toStrictEqual({ value: "stop" }); + + type check = ExpectType< + typeof result, + DFlow.Break<"stop">, + "strict" + >; + }); + + it("uses undefined as the default break value", () => { + const result = DFlow.createBreak(); + + expect(DFlow.breakKind.has(result)).toBe(true); + expect(DFlow.breakKind.getValue(result)).toStrictEqual({ value: undefined }); + }); +}); diff --git a/tests/flow/theflow/create.test.ts b/tests/flow/theflow/create.test.ts new file mode 100644 index 000000000..29648b703 --- /dev/null +++ b/tests/flow/theflow/create.test.ts @@ -0,0 +1,23 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("create", () => { + it("wraps a flow function in the flow kind and keeps the same run reference", () => { + const run = function *(input: number) { + yield DFlow.createStep("created"); + return input + 1; + }; + + const result = DFlow.create(run); + + expect(DFlow.theFlowKind.getValue(result)).toStrictEqual({ + run, + }); + expect(DFlow.theFlowKind.getValue(result).run).toBe(run); + + type check = ExpectType< + typeof result, + DFlow.TheFlow, + "strict" + >; + }); +}); diff --git a/tests/flow/theflow/defer.test.ts b/tests/flow/theflow/defer.test.ts new file mode 100644 index 000000000..d01031718 --- /dev/null +++ b/tests/flow/theflow/defer.test.ts @@ -0,0 +1,19 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("createDefer", () => { + it("creates a defer kind with the provided callback", () => { + const spy = vi.fn(() => "done" as const); + const result = DFlow.createDefer(spy); + + expect(DFlow.deferKind.has(result)).toBe(true); + expect(DFlow.deferKind.getValue(result)).toBe(spy); + expect(DFlow.deferKind.getValue(result)()).toBe("done"); + expect(spy).toHaveBeenCalledTimes(1); + + type check = ExpectType< + typeof result, + DFlow.Defer<"done">, + "strict" + >; + }); +}); diff --git a/tests/flow/theflow/dependence.test.ts b/tests/flow/theflow/dependence.test.ts new file mode 100644 index 000000000..c4df1da6c --- /dev/null +++ b/tests/flow/theflow/dependence.test.ts @@ -0,0 +1,25 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("createDependence", () => { + it("creates a dependence handler kind that returns the injected implementation", () => { + const result = DFlow.createDependence("service")<{ name: string }>; + const implementation = { + name: "service", + } as const; + + expect(DFlow.dependenceHandlerKind.has(result)).toBe(true); + expect(DFlow.dependenceHandlerKind.getValue(result)).toBe("service"); + expect(result(implementation)).toBe(implementation); + + type check = ExpectType< + typeof result, + & DFlow.DependenceHandlerKind<"service"> + & ((implementation: { + name: string; + }) => { + name: string; + }), + "strict" + >; + }); +}); diff --git a/tests/flow/theflow/exit.test.ts b/tests/flow/theflow/exit.test.ts new file mode 100644 index 000000000..a8dc8c62f --- /dev/null +++ b/tests/flow/theflow/exit.test.ts @@ -0,0 +1,24 @@ + +import { DFlow, type ExpectType } from "@scripts"; + +describe("createExit", () => { + it("creates an exit kind with the provided value", () => { + const result = DFlow.createExit("stop" as const); + + expect(DFlow.exitKind.has(result)).toBe(true); + expect(DFlow.exitKind.getValue(result)).toStrictEqual({ value: "stop" }); + + type check = ExpectType< + typeof result, + DFlow.Exit<"stop">, + "strict" + >; + }); + + it("uses undefined as the default exit value", () => { + const result = DFlow.createExit(); + + expect(DFlow.exitKind.has(result)).toBe(true); + expect(DFlow.exitKind.getValue(result)).toStrictEqual({ value: undefined }); + }); +}); diff --git a/tests/flow/theflow/finalizer.test.ts b/tests/flow/theflow/finalizer.test.ts new file mode 100644 index 000000000..ccbaaac59 --- /dev/null +++ b/tests/flow/theflow/finalizer.test.ts @@ -0,0 +1,19 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("createFinalizer", () => { + it("creates a finalizer kind with the provided callback", () => { + const spy = vi.fn(() => "done" as const); + const result = DFlow.createFinalizer(spy); + + expect(DFlow.finalizerKind.has(result)).toBe(true); + expect(DFlow.finalizerKind.getValue(result)).toBe(spy); + expect(DFlow.finalizerKind.getValue(result)()).toBe("done"); + expect(spy).toHaveBeenCalledTimes(1); + + type check = ExpectType< + typeof result, + DFlow.Finalizer<"done">, + "strict" + >; + }); +}); diff --git a/tests/flow/theflow/injection.test.ts b/tests/flow/theflow/injection.test.ts new file mode 100644 index 000000000..f3107efa5 --- /dev/null +++ b/tests/flow/theflow/injection.test.ts @@ -0,0 +1,28 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("createInjection", () => { + it("creates an injection kind with the provided dependence handler and injector", () => { + const dependence = DFlow.createDependence("service"); + const inject = vi.fn(); + const result = DFlow.createInjection({ + dependenceHandler: dependence, + inject, + }); + + expect(DFlow.injectionKind.has(result)).toBe(true); + expect(DFlow.injectionKind.getValue(result)).toStrictEqual({ + dependenceHandler: dependence, + inject, + }); + + DFlow.injectionKind.getValue(result).inject("service"); + + expect(inject).toHaveBeenCalledWith("service"); + + type check = ExpectType< + typeof result, + DFlow.Injection, + "strict" + >; + }); +}); diff --git a/tests/flow/theflow/step.test.ts b/tests/flow/theflow/step.test.ts new file mode 100644 index 000000000..d1110bd9d --- /dev/null +++ b/tests/flow/theflow/step.test.ts @@ -0,0 +1,16 @@ +import { DFlow, type ExpectType } from "@scripts"; + +describe("createStep", () => { + it("creates a step kind with the provided name", () => { + const result = DFlow.createStep("my-step"); + + expect(DFlow.stepKind.has(result)).toBe(true); + expect(DFlow.stepKind.getValue(result)).toBe("my-step"); + + type check = ExpectType< + typeof result, + DFlow.Step<"my-step">, + "strict" + >; + }); +});