From ef97ab1d2a7cb275ea23d19464823f19c1cdfb80 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 12:37:15 -0500 Subject: [PATCH 01/36] improve: anyware builder --- src/ClientPreset/ClientPreset.ts | 4 +- src/client/builderExtensions/anyware.ts | 6 +- src/client/builderExtensions/scalar.test-d.ts | 8 +- src/client/gql/gql.ts | 3 +- src/documentBuilder/InferResult/Alias.ts | 4 +- src/extension/extension.ts | 4 +- src/lib/anyware/Interceptor.ts | 62 ---- .../anyware/Interceptor/Interceptor.test-d.ts | 52 +++ src/lib/anyware/Interceptor/Interceptor.ts | 106 ++++++ src/lib/anyware/Pipeline/Pipeline.ts | 38 -- src/lib/anyware/Pipeline/_.ts | 2 + src/lib/anyware/Pipeline/__.ts | 5 + src/lib/anyware/Pipeline/builder.test-d.ts | 70 ++++ src/lib/anyware/Pipeline/builder.ts | 190 ++++++---- src/lib/anyware/Pipeline/run.test-d.ts | 20 ++ src/lib/anyware/Pipeline/run.ts | 24 ++ src/lib/anyware/_.ts | 5 +- src/lib/anyware/__.test-d.ts | 165 ++++----- src/lib/anyware/__.test-helpers.ts | 186 +++++----- src/lib/anyware/__.test.ts | 2 +- src/lib/anyware/hook/private.ts | 2 +- src/lib/anyware/hook/public.ts | 49 ++- src/lib/anyware/run/getEntrypoint.ts | 2 +- src/lib/anyware/run/runHook.ts | 12 +- src/lib/anyware/run/runPipeline.ts | 4 +- src/lib/anyware/run/runner.ts | 18 +- src/lib/builder/Definition.ts | 12 +- .../config-manager/ConfigManager.test-d.ts | 2 +- src/lib/config-manager/ConfigManager.ts | 14 +- src/lib/prelude.test-d.ts | 17 +- src/lib/prelude.ts | 51 ++- src/requestPipeline/RequestPipeline.ts | 336 ++++++++++++------ src/requestPipeline/_.ts | 2 - src/requestPipeline/__.ts | 2 +- src/requestPipeline/types.ts | 123 ------- 35 files changed, 925 insertions(+), 677 deletions(-) delete mode 100644 src/lib/anyware/Interceptor.ts create mode 100644 src/lib/anyware/Interceptor/Interceptor.test-d.ts create mode 100644 src/lib/anyware/Interceptor/Interceptor.ts delete mode 100644 src/lib/anyware/Pipeline/Pipeline.ts create mode 100644 src/lib/anyware/Pipeline/_.ts create mode 100644 src/lib/anyware/Pipeline/__.ts create mode 100644 src/lib/anyware/Pipeline/builder.test-d.ts create mode 100644 src/lib/anyware/Pipeline/run.test-d.ts create mode 100644 src/lib/anyware/Pipeline/run.ts delete mode 100644 src/requestPipeline/_.ts delete mode 100644 src/requestPipeline/types.ts diff --git a/src/ClientPreset/ClientPreset.ts b/src/ClientPreset/ClientPreset.ts index b562b1922..f8be27b87 100644 --- a/src/ClientPreset/ClientPreset.ts +++ b/src/ClientPreset/ClientPreset.ts @@ -14,7 +14,7 @@ import type { } from '../extension/extension.js' import type { Builder } from '../lib/builder/__.js' import type { ConfigManager } from '../lib/config-manager/__.js' -import { type mergeArrayOfObjects, type ToParametersExact } from '../lib/prelude.js' +import { type intersectArrayOfObjects, type ToParametersExact } from '../lib/prelude.js' import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js' import { Schema } from '../types/Schema/__.js' import type { SchemaDrivenDataMap } from '../types/SchemaDrivenDataMap/__.js' @@ -101,7 +101,7 @@ type ConstructorParameters< $Extensions extends [...ExtensionConstructor[]], > = & InputBase> - & mergeArrayOfObjects> + & intersectArrayOfObjects> // dprint-ignore type GetParametersContributedByExtensions = { diff --git a/src/client/builderExtensions/anyware.ts b/src/client/builderExtensions/anyware.ts index c5efceeb7..79196b61d 100644 --- a/src/client/builderExtensions/anyware.ts +++ b/src/client/builderExtensions/anyware.ts @@ -15,13 +15,15 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters>, + interceptor: AnywareLib.InferInterceptorConstructor< + RequestPipeline.RequestPipeline<$Arguments['context']['config']> + >, ) => Builder.Definition.MaterializeWithNewContext<$Arguments['chain'], $Arguments['context']> } export const builderExtensionAnyware = Builder.Extension.create((builder, context) => { const properties = { - anyware: (interceptor: Anyware.Interceptor) => { + anyware: (interceptor: Anyware.InferInterceptorConstructor) => { return builder({ ...context, extensions: [ diff --git a/src/client/builderExtensions/scalar.test-d.ts b/src/client/builderExtensions/scalar.test-d.ts index f5e2e069e..08051dda3 100644 --- a/src/client/builderExtensions/scalar.test-d.ts +++ b/src/client/builderExtensions/scalar.test-d.ts @@ -2,7 +2,7 @@ import { DateScalar, FooScalar } from '../../../tests/_/fixtures/scalars.js' import { schemaMap } from '../../../tests/_/schemas/kitchen-sink/graffle/__.js' import { Graffle } from '../../entrypoints/__Graffle.js' import { assertEqual } from '../../lib/assert-equal.js' -import { any, type SomeFunction } from '../../lib/prelude.js' +import { _, type SomeFunction } from '../../lib/prelude.js' import type { TypeErrorMissingSchemaMap } from './scalar.js' const g1 = Graffle.create({ schema: `foo` }) @@ -12,9 +12,9 @@ const g2 = Graffle.create({ schema: `foo`, schemaMap }) assertEqual() // @ts-expect-error "Foo" is not a scalar name in the schema. -Graffle.create({ schema: `foo`, schemaMap }).scalar(`Foo`, any) +Graffle.create({ schema: `foo`, schemaMap }).scalar(`Foo`, _) // @ts-expect-error "Foo" is not a scalar name in the schema. Graffle.create({ schema: `foo`, schemaMap }).scalar(FooScalar) -Graffle.create({ schema: `foo`, schemaMap }).scalar(`Date`, any) +Graffle.create({ schema: `foo`, schemaMap }).scalar(`Date`, _) Graffle.create({ schema: `foo`, schemaMap }).scalar(DateScalar) -Graffle.create({ schema: `foo`, schemaMap }).scalar(`Int`, any) +Graffle.create({ schema: `foo`, schemaMap }).scalar(`Int`, _) diff --git a/src/client/gql/gql.ts b/src/client/gql/gql.ts index e81c220a7..57168d17b 100644 --- a/src/client/gql/gql.ts +++ b/src/client/gql/gql.ts @@ -1,3 +1,4 @@ +import { Anyware } from '../../lib/anyware/__.js' import { Builder } from '../../lib/builder/__.js' import type { Grafaid } from '../../lib/grafaid/__.js' import { getOperationType } from '../../lib/grafaid/document.js' @@ -73,7 +74,7 @@ export const builderExtensionGql = Builder.Extension.create request: analyzedRequest, } as RequestPipeline.Hooks.HookDefEncode['input'] - const result = await RequestPipeline.RequestPipeline.run({ + const result = await Anyware.Pipeline.run(RequestPipeline, { initialInput, // retryingExtension: context.retry as any, interceptors: context.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, diff --git a/src/documentBuilder/InferResult/Alias.ts b/src/documentBuilder/InferResult/Alias.ts index 029f6c7b4..c64c493b8 100644 --- a/src/documentBuilder/InferResult/Alias.ts +++ b/src/documentBuilder/InferResult/Alias.ts @@ -1,4 +1,4 @@ -import type { mergeArrayOfObjects, ValuesOrEmptyObject } from '../../lib/prelude.js' +import type { intersectArrayOfObjects, ValuesOrEmptyObject } from '../../lib/prelude.js' import type { Schema } from '../../types/Schema/__.js' import type { Select } from '../Select/__.js' import type { OutputField } from './OutputField.js' @@ -42,7 +42,7 @@ type InferSelectAliasMultiple< $FieldName extends string, $Schema extends Schema, $Node extends Schema.OutputObject, -> = mergeArrayOfObjects< +> = intersectArrayOfObjects< { [_ in keyof $SelectAliasMultiple]: InferSelectAliasOne<$SelectAliasMultiple[_], $FieldName, $Schema, $Node> } diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 5b5320229..cfc81f1ed 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -57,7 +57,7 @@ export interface Extension< /** * Anyware executed on every request. */ - onRequest?: Anyware.Interceptor + onRequest?: Anyware.Interceptor.InferConstructor /** * Manipulate the builder. * You can extend the builder with new properties at both runtime AND buildtime (types, TypeScript). @@ -169,7 +169,7 @@ export const createExtension = < custom?: $Custom create: (params: { config: $Config }) => { builder?: $BuilderExtension - onRequest?: Anyware.Interceptor + onRequest?: Anyware.Interceptor.InferConstructor typeHooks?: () => $TypeHooks } }, diff --git a/src/lib/anyware/Interceptor.ts b/src/lib/anyware/Interceptor.ts deleted file mode 100644 index 33a84c0d8..000000000 --- a/src/lib/anyware/Interceptor.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Deferred, MaybePromise } from '../prelude.js' -import type { Private } from '../private.js' -import type { InferPublicHooks, SomePublicHookEnvelope } from './hook/public.js' -import type { Pipeline } from './Pipeline/Pipeline.js' - -export type InterceptorOptions = { - retrying: boolean -} - -export type Interceptor< - $Core extends Pipeline = Pipeline, - $Options extends InterceptorOptions = InterceptorOptions, -> = ( - hooks: InferPublicHooks< - Private.Get<$Core>['hookSequence'], - Private.Get<$Core>['hookMap'], - Private.Get<$Core>['result'], - $Options - >, -) => Promise< - | Private.Get<$Core>['result'] - | SomePublicHookEnvelope -> - -export type InterceptorGeneric = NonRetryingInterceptor | RetryingInterceptor - -export type NonRetryingInterceptor = { - retrying: false - name: string - entrypoint: string - body: Deferred - currentChunk: Deferred -} - -export type RetryingInterceptor = { - retrying: true - name: string - entrypoint: string - body: Deferred - currentChunk: Deferred -} - -export const createRetryingInterceptor = (extension: NonRetryingInterceptorInput): RetryingInterceptorInput => { - return { - retrying: true, - run: extension, - } -} - -// export type ExtensionInput<$Input extends object = object> = (input: $Input) => MaybePromise -export type InterceptorInput<$Input extends object = any> = - | NonRetryingInterceptorInput<$Input> - | RetryingInterceptorInput<$Input> - -export type NonRetryingInterceptorInput<$Input extends object = any> = ( - input: $Input, -) => MaybePromise - -export type RetryingInterceptorInput<$Input extends object = any> = { - retrying: boolean - run: (input: $Input) => MaybePromise -} diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts new file mode 100644 index 000000000..9332842fb --- /dev/null +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -0,0 +1,52 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { _ } from '../../prelude.js' +import { type Interceptor, Pipeline } from '../_.js' +import type { initialInput } from '../__.test-helpers.js' +import { results, slots } from '../__.test-helpers.js' +import type { SomePublicStepEnvelope } from '../hook/public.js' + +const p1 = Pipeline.create() + .step({ name: `a`, run: () => results.a }) + .step({ name: `b`, run: () => results.b }) + .step({ name: `c`, run: () => results.c }) + +type p1 = typeof p1 + +describe(`interceptor constructor`, () => { + type i1 = Interceptor.InferConstructor + + test(`receives keyword arguments, a step trigger for each step`, () => { + expectTypeOf>().toMatchTypeOf<[steps: { a: any; b: any; c: any }]>() + expectTypeOf>().toMatchTypeOf<[steps: { + a: (params: { input?: initialInput }) => Promise<{ b: (params: { input?: results['a'] }) => any }> + b: (params: { input?: results['a'] }) => Promise<{ c: (params: { input?: results['b'] }) => any }> + c: (params: { input?: results['b'] }) => Promise + }]>() + }) + + test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { + const p = Pipeline.create().step({ + name: `a`, + slots: { m: slots.m }, + run: () => Promise.resolve(results.a), + }) + .step({ name: `b`, run: () => results.b }) + type i = Interceptor.InferConstructor + type stepAParameters = Parameters[0]['a']> + expectTypeOf().toEqualTypeOf<[params: { input?: initialInput; slots?: { m?: slots['m'] } }]> + type stepBParameters = Parameters[0]['b']> + expectTypeOf().toEqualTypeOf<[params: { input?: results['a'] }]> // no "slots" key! + }) + + // --- return --- + + test(`can return pipeline output or a step envelope`, () => { + expectTypeOf>().toEqualTypeOf>() + }) + + test(`return type awaits pipeline output`, () => { + const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(results.a) }) + type i = Interceptor.InferConstructor + expectTypeOf>().toEqualTypeOf>() + }) +}) diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts new file mode 100644 index 000000000..72beeba46 --- /dev/null +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -0,0 +1,106 @@ +import type { Simplify } from 'type-fest' +import type { Deferred, intersectArrayOfObjects, MaybePromise, Tuple } from '../../prelude.js' +import type { Private } from '../../private.js' +import type { InferPublicHooks, SomePublicStepEnvelope } from '../hook/public.js' +import type { Pipeline } from '../Pipeline/__.js' +import type { Step } from '../Pipeline/builder.js' + +export type InterceptorOptions = { + retrying: boolean +} + +// todo +export interface Interceptor { + name: string + // entrypoint: string + // body: Deferred + // currentChunk: Deferred +} + +export namespace Interceptor { + export type InferConstructor< + $Pipeline extends Pipeline = Pipeline, + > // $Options extends InterceptorOptions = InterceptorOptions, + = ( + steps: Simplify>, + ) => Promise< + | Pipeline.GetAwaitedResult<$Pipeline> + | SomePublicStepEnvelope + > + + type InferConstructorKeywordArguments< + $Pipeline extends Pipeline, + > = InferConstructorKeywordArguments_<$Pipeline['steps'], Pipeline.GetAwaitedResult<$Pipeline>> + + // dprint-ignore + type InferConstructorKeywordArguments_< + $Steps extends Step[], + $PipelineOutput, + > = + $Steps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] + ? & { + [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> + } + & InferConstructorKeywordArguments_<$NextNextSteps, $PipelineOutput> + : {} + + // dprint-ignore + type InferStepTrigger<$Step extends Step, $NextSteps extends Step[], $PipelineOutput> = + ( + params: Simplify< + & { + input?: $Step['input'] + } + & ( + $Step['slots'] extends undefined + ? {} + : { slots?: Partial<$Step['slots']> } + ) + > + ) => Promise< + $NextSteps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] + ? { + [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> + } + : $PipelineOutput + > +} + +export type InterceptorGeneric = NonRetryingInterceptor | RetryingInterceptor + +export type NonRetryingInterceptor = { + retrying: false + name: string + entrypoint: string + body: Deferred + currentChunk: Deferred +} + +export type RetryingInterceptor = { + retrying: true + name: string + entrypoint: string + body: Deferred + currentChunk: Deferred +} + +export const createRetryingInterceptor = (extension: NonRetryingInterceptorInput): RetryingInterceptorInput => { + return { + retrying: true, + run: extension, + } +} + +// export type ExtensionInput<$Input extends object = object> = (input: $Input) => MaybePromise +export type InterceptorInput<$Input extends object = any> = + | NonRetryingInterceptorInput<$Input> + | RetryingInterceptorInput<$Input> + +export type NonRetryingInterceptorInput<$Input extends object = any> = ( + input: $Input, +) => MaybePromise + +export type RetryingInterceptorInput<$Input extends object = any> = { + retrying: boolean + run: (input: $Input) => MaybePromise +} diff --git a/src/lib/anyware/Pipeline/Pipeline.ts b/src/lib/anyware/Pipeline/Pipeline.ts deleted file mode 100644 index 8a789748a..000000000 --- a/src/lib/anyware/Pipeline/Pipeline.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { FindValueAfter, IsLastValue } from '../../prelude.js' -import type { Private } from '../../private.js' -import type { HookDefinitionMap, HookSequence } from '../hook/definition.js' -import type { HookPrivateInput, HookResultError, PrivateHook } from '../hook/private.js' -export * as Pipeline from './builder.js' - -// dprint-ignore -export type Pipeline< - $HookSequence extends HookSequence = HookSequence, - $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - $Result = unknown, -> = - // todo what is the point of private here? Its usually for hiding fields on a user facing interface. But is this type actually user facing? - Private.Add< - { - hookSequence: $HookSequence - hookMap: $HookMap - result: $Result - }, - { - hookNamesOrderedBySequence: $HookSequence - hooks: { - [$HookName in $HookSequence[number]]: - PrivateHook< - $HookMap[$HookName]['slots'], - HookPrivateInput< - $HookMap[$HookName]['input'], - $HookMap[$HookName]['slots'] - >, - IsLastValue<$HookName, $HookSequence> extends true - ? $Result - : $HookMap[FindValueAfter<$HookName, $HookSequence>] - > - } - passthroughErrorInstanceOf?: Function[] - passthroughErrorWith?: (signal: HookResultError) => boolean - } - > diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts new file mode 100644 index 000000000..07fa5d5d4 --- /dev/null +++ b/src/lib/anyware/Pipeline/_.ts @@ -0,0 +1,2 @@ +export * from './builder.js' +export * from './run.js' diff --git a/src/lib/anyware/Pipeline/__.ts b/src/lib/anyware/Pipeline/__.ts new file mode 100644 index 000000000..f14d876b6 --- /dev/null +++ b/src/lib/anyware/Pipeline/__.ts @@ -0,0 +1,5 @@ +import type { Context } from './builder.js' + +export * as Pipeline from './_.js' + +export type Pipeline = Context diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/Pipeline/builder.test-d.ts new file mode 100644 index 000000000..5746e902b --- /dev/null +++ b/src/lib/anyware/Pipeline/builder.test-d.ts @@ -0,0 +1,70 @@ +import { expectTypeOf, test } from 'vitest' +import type { initialInput } from '../__.test-helpers.js' +import { results, slots } from '../__.test-helpers.js' +import { Pipeline } from './__.js' + +const p0 = Pipeline.create() + +test(`initial context`, () => { + expectTypeOf(p0.context).toEqualTypeOf<{ input: initialInput; output: object; steps: [] }>() +}) + +test(`first step definition`, () => { + expectTypeOf(p0.step).toMatchTypeOf< + (input: { name: string; run: (params: { input: initialInput; previous: undefined }) => any }) => any + >() +}) + +test(`second step definition`, () => { + const p1 = p0.step({ name: `a`, run: () => results.a }) + expectTypeOf(p1.step).toMatchTypeOf< + ( + input: { + name: string + run: (params: { + input: results['a'] + slots: undefined + previous: { a: { output: results['a'] } } + }) => any + }, + ) => any + >() + expectTypeOf(p1.context).toMatchTypeOf< + { + input: initialInput + output: object + steps: [{ name: 'a'; slots: undefined; run: any }] + } + >() +}) + +test(`step definition with slots`, () => { + const p1 = p0 + .step({ + name: `a`, + slots: { + m: slots.m, + n: slots.n, + }, + run: ({ slots }) => { + expectTypeOf(slots.m()).toEqualTypeOf>() + expectTypeOf(slots.n()).toEqualTypeOf<'n'>() + return results.a + }, + }) + expectTypeOf(p1.context).toMatchTypeOf< + { + input: initialInput + output: object + steps: [{ + name: 'a' + slots: slots + run: (params: { + input: initialInput + slots: slots + previous: undefined + }) => any + }] + } + >() +}) diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 94745c8ee..f6f8cf08c 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,83 +1,133 @@ -import { type FindValueAfter, type IsLastValue, type MaybePromise } from '../../prelude.js' -import type { HookDefinitionMap, HookSequence } from '../hook/definition.js' -import type { HookResultError, InferPrivateHookInput } from '../hook/private.js' -import { createRunner, type Runner } from '../run/runner.js' -import type { Pipeline } from './Pipeline.js' +import type { ConfigManager } from '../../config-manager/__.js' +import { type GetLastValue, type intersectArrayOfObjects } from '../../prelude.js' +import type { Pipeline } from './__.js' export { type HookDefinitionMap } from '../hook/definition.js' +export interface Step< + $Name extends string = string, +> { + name: $Name + slots: object | undefined + input: any + output: any + run: (params: any) => any +} -export type PipelineInput< - $HookSequence extends HookSequence = HookSequence, - $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - $Result = unknown, -> = { - hookNamesOrderedBySequence: $HookSequence - // dprint-ignore - hooks: { - [$HookName in $HookSequence[number]]: - keyof $HookMap[$HookName]['slots'] extends never - ? (input: InferPrivateHookInput<$HookSequence, $HookMap, $HookName>) => - MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true - ? $Result - : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] - > - : { - slots: $HookMap[$HookName]['slots'] - run: (input: InferPrivateHookInput<$HookSequence, $HookMap, $HookName>) => - MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true ? $Result - : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] - > - } - } - /** - * If a hook results in a thrown error but is an instance of one of these classes then return it as-is - * rather than wrapping it in a ContextualError. - * - * This can be useful when there are known kinds of errors such as Abort Errors from AbortController - * which are actually a signaling mechanism. - */ - passthroughErrorInstanceOf?: Function[] - /** - * If a hook results in a thrown error but returns true from this function then return the error as-is - * rather than wrapping it in a ContextualError. - * - * This can be useful when there are known kinds of errors such as Abort Errors from AbortController - * which are actually a signaling mechanism. - */ - passthroughErrorWith?: (signal: HookResultError) => boolean +export interface Context { + input: object + steps: Step[] } -export const create = < - $HookSequence extends HookSequence = HookSequence, - $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - $Result = unknown, ->( - input: PipelineInput<$HookSequence, $HookMap, $Result>, -): Builder> => { - type $Pipeline = Pipeline<$HookSequence, $HookMap, $Result> +export interface ContextEmpty extends Context { + input: object + output: object + steps: [] +} - const pipeline = { - ...input, - hooks: Object.fromEntries( - Object.entries(input.hooks).map(([k, v]) => { - return [k, typeof v === `function` ? { slots: {}, run: v } : v] - }), - ), - } as any as $Pipeline +export namespace Step { + export type GetAwaitedResult<$Step extends Step> = Awaited> + export type GetResult<$Step extends Step> = ReturnType<$Step['run']> +} - const run = createRunner(pipeline) +/** + * See {@link GetResult} + */ +export type GetAwaitedResult<$Pipeline extends Pipeline> = Awaited> - const builder: Builder<$Pipeline> = { - pipeline, - run, +/** + * Get the overall result of the pipeline. + * + * If the pipeline has no steps then the pipeline input itself. + * Otherwise the last step's output. + */ +// dprint-ignore +export type GetResult<$Pipeline extends Pipeline> = + $Pipeline['steps'] extends [any, ...any[]] + ? Step.GetResult> + : $Pipeline['input'] + +// dprint-ignore +export type GetNextStepParameterPrevious<$Pipeline extends Pipeline> = + $Pipeline['steps'] extends [any, ...any[]] + ? GetNextStepPrevious_<$Pipeline['steps']> + : undefined + +type GetNextStepPrevious_<$Steps extends Step[]> = intersectArrayOfObjects< + { + [$Index in keyof $Steps]: { + [$StepName in $Steps[$Index]['name']]: { + input: Parameters<$Steps[$Index]['run']>[0]['input'] + output: Awaited> + } + } } +> - return builder +/** + * Get the `input` parameter for a step that would be appended to the given Pipeline. + * + * Recall that non-first steps have input corresponding to the output of the previous step. + * + * So this returns: + * - If the pipeline has no steps then the pipeline input itself. + * - Otherwise the last step's output. + */ +// dprint-ignore +type GetNextStepParameterInput<$Pipeline extends Pipeline> = + $Pipeline['steps'] extends [any, ...any[]] + ? Step.GetResult> + : $Pipeline['input'] + +export interface Builder<$Context extends Context = Context> { + context: $Context + /** + * todo + */ + step: < + const $Name extends string, + $Run extends (params: { + input: GetNextStepParameterInput<$Context> + slots: $Slots + previous: GetNextStepParameterPrevious<$Context> + }) => any, + $Slots extends undefined | Record = undefined, + $Params extends { + input: GetNextStepParameterInput<$Context> + slots: $Slots + previous: GetNextStepParameterPrevious<$Context> + } = { + input: GetNextStepParameterInput<$Context> + slots: $Slots + previous: GetNextStepParameterPrevious<$Context> + }, + >( + stepInput: { + name: $Name + slots?: $Slots + run: $Run + }, + ) => Builder< + ConfigManager.SetOneKey< + $Context, + 'steps', + [...$Context['steps'], { + name: $Name + run: $Run + input: $Params['input'] + output: ReturnType<$Run> + slots: $Slots + }] + > + > } -export type Builder<$Pipeline extends Pipeline> = { - pipeline: $Pipeline - run: Runner<$Pipeline> +// export type Pipeline = Context + +export type Infer<$Builder extends Builder> = $Builder['context'] + +/** + * TODO + */ +export const create = <$Input extends object>(): Builder<{ input: $Input; steps: []; output: object }> => { + return undefined as any } diff --git a/src/lib/anyware/Pipeline/run.test-d.ts b/src/lib/anyware/Pipeline/run.test-d.ts new file mode 100644 index 000000000..521e229b1 --- /dev/null +++ b/src/lib/anyware/Pipeline/run.test-d.ts @@ -0,0 +1,20 @@ +import { expectTypeOf, test } from 'vitest' +import { Pipeline } from './__.js' + +test(`returns input if no steps`, () => { + const p = Pipeline.create<{ x: 1 }>() + const r = Pipeline.run(p) + expectTypeOf(r).toEqualTypeOf<{ x: 1 }>() +}) + +test(`returns last step output if steps`, () => { + const p = Pipeline.create<{ x: 1 }>().step({ name: `a`, run: () => 2 as const }) + const r = Pipeline.run(p) + expectTypeOf(r).toEqualTypeOf<2>() +}) + +test(`can return a promise`, async () => { + const p = Pipeline.create<{ x: 1 }>().step({ name: `a`, run: () => Promise.resolve(2 as const) }) + const r = await Pipeline.run(p) + expectTypeOf(r).toEqualTypeOf<2>() +}) diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts new file mode 100644 index 000000000..9aec1ed99 --- /dev/null +++ b/src/lib/anyware/Pipeline/run.ts @@ -0,0 +1,24 @@ +import type { Interceptor } from '../Interceptor/Interceptor.js' +import type { Pipeline } from './__.js' +import type { Builder } from './builder.js' + +interface Params { + initialInput: object + interceptors: Interceptor[] +} + +type Run = < + $Builder extends Builder, + $Params extends Params, +>( + pipeline: $Builder, + params?: $Params, +) => Pipeline.GetResult<$Builder['context']> + +/** + * todo + */ +export const run: Run = (pipeline, params) => { + // todo + return undefined as any +} diff --git a/src/lib/anyware/_.ts b/src/lib/anyware/_.ts index 917c6e340..bc46832d5 100644 --- a/src/lib/anyware/_.ts +++ b/src/lib/anyware/_.ts @@ -1,4 +1,3 @@ -export * from './Interceptor.js' -export * from './Pipeline/builder.js' -export * from './Pipeline/Pipeline.js' +export * from './Interceptor/Interceptor.js' +export * from './Pipeline/__.js' export * from './run/runner.js' diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts index 323d3f42d..16aa76fb3 100644 --- a/src/lib/anyware/__.test-d.ts +++ b/src/lib/anyware/__.test-d.ts @@ -1,103 +1,86 @@ /* eslint-disable */ import { describe, expectTypeOf, test } from 'vitest' +import { assertEqual } from '../assert-equal.js' import { ContextualError } from '../errors/ContextualError.js' import { type MaybePromise } from '../prelude.js' import { Anyware } from './__.js' -import type { PublicHook } from './hook/public.js' +import type { PublicStep } from './hook/public.js' -type InputA = { valueA: string } -type InputB = { valueB: string } -type Result = { return: string } +// describe('without slots', () => { +// test('run', () => { +// type run = ReturnType['run'] -const create = Anyware.create<['a', 'b'], { a: { input: InputA }; b: { input: InputB } }, Result> +// expectTypeOf().toMatchTypeOf< +// (input: { +// initialInput: InputA +// options?: Anyware.Options +// retryingExtension?: (input: { +// a: PublicHook< +// (params?: { input?: InputA }) => MaybePromise< +// Error | { +// b: PublicHook< +// (params?: { input?: InputB }) => MaybePromise +// > +// } +// > +// > +// b: PublicHook<(params?: { input?: InputB }) => MaybePromise> +// }) => Promise +// interceptors: ((input: { +// a: PublicHook< +// (params?: { input?: InputA }) => MaybePromise<{ +// b: PublicHook<(params?: { input?: InputB }) => MaybePromise> +// }> +// > +// b: PublicHook<(params?: { input?: InputB }) => MaybePromise> +// }) => Promise)[] +// }) => Promise +// >() +// }) +// }) -describe('without slots', () => { - test('create', () => { - expectTypeOf(create).toMatchTypeOf< - (input: { - hookNamesOrderedBySequence: ['a', 'b'] - hooks: { - a: (input: { input: InputA; previous: {} }) => InputB - b: (input: { input: InputB; previous: { a: { input: InputA } } }) => Result - } - }) => any - >() - }) +// describe('withSlots', () => { +// const create = Anyware.create<['a'], { a: { input: InputA; slots: { x: (x: boolean) => number } } }, Result> - test('run', () => { - type run = ReturnType['run'] +// test('create', () => { +// expectTypeOf(create).toMatchTypeOf< +// (input: { +// hookNamesOrderedBySequence: ['a'] +// hooks: { +// a: { +// run: (input: { input: InputA; previous: {} }) => Result +// slots: { +// x: (x: boolean) => number +// } +// } +// } +// }) => any +// >() +// }) - expectTypeOf().toMatchTypeOf< - (input: { - initialInput: InputA - options?: Anyware.Options - retryingExtension?: (input: { - a: PublicHook< - (params?: { input?: InputA }) => MaybePromise< - Error | { - b: PublicHook< - (params?: { input?: InputB }) => MaybePromise - > - } - > - > - b: PublicHook<(params?: { input?: InputB }) => MaybePromise> - }) => Promise - interceptors: ((input: { - a: PublicHook< - (params?: { input?: InputA }) => MaybePromise<{ - b: PublicHook<(params?: { input?: InputB }) => MaybePromise> - }> - > - b: PublicHook<(params?: { input?: InputB }) => MaybePromise> - }) => Promise)[] - }) => Promise - >() - }) -}) +// test('run', () => { +// type run = ReturnType['run'] -describe('withSlots', () => { - const create = Anyware.create<['a'], { a: { input: InputA; slots: { x: (x: boolean) => number } } }, Result> - - test('create', () => { - expectTypeOf(create).toMatchTypeOf< - (input: { - hookNamesOrderedBySequence: ['a'] - hooks: { - a: { - run: (input: { input: InputA; previous: {} }) => Result - slots: { - x: (x: boolean) => number - } - } - } - }) => any - >() - }) - - test('run', () => { - type run = ReturnType['run'] - - expectTypeOf().toMatchTypeOf< - (input: { - initialInput: InputA - options?: Anyware.Options - interceptors: ((input: { - a: PublicHook< - ( - input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }, - ) => MaybePromise - > - }) => Promise)[] - retryingExtension?: (input: { - a: PublicHook< - (input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }) => MaybePromise< - Error | Result - > - > - }) => Promise - }) => Promise - >() - }) -}) +// expectTypeOf().toMatchTypeOf< +// (input: { +// initialInput: InputA +// options?: Anyware.Options +// interceptors: ((input: { +// a: PublicHook< +// ( +// input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }, +// ) => MaybePromise +// > +// }) => Promise)[] +// retryingExtension?: (input: { +// a: PublicHook< +// (input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }) => MaybePromise< +// Error | Result +// > +// > +// }) => Promise +// }) => Promise +// >() +// }) +// }) diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index b75624bf9..941b9dab5 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -1,101 +1,117 @@ import type { Mock } from 'vitest' import { beforeEach, vi } from 'vitest' import { Anyware } from './__.js' -import type { InterceptorInput } from './Interceptor.js' +import type { InterceptorInput } from './Interceptor/Interceptor.js' import type { Options } from './run/runner.js' -type PrivateHookRunnerInput = { - input: { value: string } - slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } - previous: object -} +export const initialInput = { x: 1 } as const +export type initialInput = typeof initialInput -type PrivateHookRunner = (input: PrivateHookRunnerInput) => any +export const results = { + a: { a: 1 }, + b: { b: 2 }, + c: { c: 3 }, +} as const +export type results = typeof results -export const initialInput: PrivateHookRunnerInput['input'] = { value: `initial` } +export const slots = { + m: () => Promise.resolve(`m` as const), + n: () => `n` as const, +} as const +export type slots = typeof slots -type $Core = ReturnType & { - hooks: { - a: { - run: Mock - slots: { - append: Mock<(hookName: string) => string> - appendExtra: Mock<(hookName: string) => string> - } - } - b: { - run: Mock - slots: { - append: Mock<(hookName: string) => string> - appendExtra: Mock<(hookName: string) => string> - } - } - } -} +// type PrivateHookRunnerInput = { +// input: { value: string } +// slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } +// previous: object +// } -export const createHook = <$Slots extends object, $Input extends object, $Result = unknown>( - $Hook: { - slots: $Slots - run: (input: { input: $Input; slots: $Slots }) => $Result - }, -) => $Hook +// type PrivateHookRunner = (input: PrivateHookRunnerInput) => any -export const createAnyware = () => { - const a = createHook({ - slots: { - append: vi.fn().mockImplementation((hookName: string) => { - return hookName - }), - appendExtra: vi.fn().mockImplementation(() => { - return `` - }), - }, - run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { - const extra = slots.appendExtra(`a`) - return { value: input.value + `+` + slots.append(`a`) + extra } - }), - }) - const b = createHook({ - slots: { - append: vi.fn().mockImplementation((hookName: string) => { - return hookName - }), - appendExtra: vi.fn().mockImplementation(() => { - return `` - }), - }, - run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { - const extra = slots.appendExtra(`b`) - return { value: input.value + `+` + slots.append(`b`) + extra } - }), - }) +// export const initialInput: PrivateHookRunnerInput['input'] = { value: `initial` } - return Anyware.create<['a', 'b'], Anyware.HookDefinitionMap<['a', 'b']>, PrivateHookRunnerInput>({ - hookNamesOrderedBySequence: [`a`, `b`], - hooks: { a, b }, - }) -} +// type $Core = ReturnType & { +// hooks: { +// a: { +// run: Mock +// slots: { +// append: Mock<(hookName: string) => string> +// appendExtra: Mock<(hookName: string) => string> +// } +// } +// b: { +// run: Mock +// slots: { +// append: Mock<(hookName: string) => string> +// appendExtra: Mock<(hookName: string) => string> +// } +// } +// } +// } -// @ts-expect-error -export let anyware: Anyware.Builder<$Core> = null -export let core: $Core +// export const createHook = <$Slots extends object, $Input extends object, $Result = unknown>( +// $Hook: { +// slots: $Slots +// run: (input: { input: $Input; slots: $Slots }) => $Result +// }, +// ) => $Hook -beforeEach(() => { - // @ts-expect-error mock types not tracked by Anyware - anyware = createAnyware() - core = anyware.pipeline -}) +// export const createAnyware = () => { +// const a = createHook({ +// slots: { +// append: vi.fn().mockImplementation((hookName: string) => { +// return hookName +// }), +// appendExtra: vi.fn().mockImplementation(() => { +// return `` +// }), +// }, +// run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { +// const extra = slots.appendExtra(`a`) +// return { value: input.value + `+` + slots.append(`a`) + extra } +// }), +// }) +// const b = createHook({ +// slots: { +// append: vi.fn().mockImplementation((hookName: string) => { +// return hookName +// }), +// appendExtra: vi.fn().mockImplementation(() => { +// return `` +// }), +// }, +// run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { +// const extra = slots.appendExtra(`b`) +// return { value: input.value + `+` + slots.append(`b`) + extra } +// }), +// }) -export const runWithOptions = (options: Options = {}) => async (...interceptors: InterceptorInput[]) => { - const result = await anyware.run({ - initialInput, - // @ts-expect-error fixme - interceptors, - options, - }) - return result -} +// return Anyware.create<['a', 'b'], Anyware.HookDefinitionMap<['a', 'b']>, PrivateHookRunnerInput>({ +// hookNamesOrderedBySequence: [`a`, `b`], +// hooks: { a, b }, +// }) +// } -export const run = async (...extensions: InterceptorInput[]) => runWithOptions({})(...extensions) +// // @ts-expect-error +// export let anyware: Anyware.Builder<$Core> = null +// export let core: $Core -export const oops = new Error(`oops`) +// beforeEach(() => { +// // @ts-expect-error mock types not tracked by Anyware +// anyware = createAnyware() +// core = anyware.pipeline +// }) + +// export const runWithOptions = (options: Options = {}) => async (...interceptors: InterceptorInput[]) => { +// const result = await anyware.run({ +// initialInput, +// // @ts-expect-error fixme +// interceptors, +// options, +// }) +// return result +// } + +// export const run = async (...extensions: InterceptorInput[]) => runWithOptions({})(...extensions) + +// export const oops = new Error(`oops`) diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts index b37aec591..242563180 100644 --- a/src/lib/anyware/__.test.ts +++ b/src/lib/anyware/__.test.ts @@ -5,7 +5,7 @@ import { Errors } from '../errors/__.js' import type { ContextualError } from '../errors/ContextualError.js' import { Anyware } from './__.js' import { core, createHook, initialInput, oops, run, runWithOptions } from './__.test-helpers.js' -import { createRetryingInterceptor } from './Interceptor.js' +import { createRetryingInterceptor } from './Interceptor/Interceptor.js' describe(`no extensions`, () => { test(`passthrough to implementation`, async () => { diff --git a/src/lib/anyware/hook/private.ts b/src/lib/anyware/hook/private.ts index fc7105e8c..a3fa4e459 100644 --- a/src/lib/anyware/hook/private.ts +++ b/src/lib/anyware/hook/private.ts @@ -1,6 +1,6 @@ import type { Errors } from '../../errors/__.js' import type { Deferred, MaybePromise, SomeFunction, TakeValuesBefore } from '../../prelude.js' -import type { InterceptorGeneric } from '../Interceptor.js' +import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' import type { HookDefinitionMap, HookSequence } from './definition.js' export type InferPrivateHookInput< diff --git a/src/lib/anyware/hook/public.ts b/src/lib/anyware/hook/public.ts index eef667859..e4a7460ca 100644 --- a/src/lib/anyware/hook/public.ts +++ b/src/lib/anyware/hook/public.ts @@ -1,31 +1,27 @@ import type { FindValueAfter, IsLastValue } from '../../prelude.js' -import type { InterceptorOptions } from '../Interceptor.js' +import type { InterceptorOptions } from '../Interceptor/Interceptor.js' +import type { Pipeline } from '../Pipeline/__.js' import type { HookDefinition, HookDefinitionMap, HookSequence } from './definition.js' export type InferPublicHooks< - $HookSequence extends HookSequence, - $HookMap extends Record<$HookSequence[number], HookDefinition> = Record<$HookSequence[number], HookDefinition>, - $Result = unknown, + $Pipeline extends Pipeline, + // $HookSequence extends HookSequence, + // $HookMap extends Record<$HookSequence[number], HookDefinition> = Record<$HookSequence[number], HookDefinition>, + // $Result = unknown, $Options extends InterceptorOptions = InterceptorOptions, > = { - [$HookName in $HookSequence[number]]: InferPublicHook<$HookSequence, $HookMap, $Result, $HookName, $Options> + [$Index in keyof $Pipeline['steps'][number]]: InferPublicHook<$Pipeline['steps'][$Index], $Options> } type InferPublicHook< - $HookSequence extends HookSequence, - $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - $Result = unknown, - $Name extends $HookSequence[number] = $HookSequence[number], + $Step extends Pipeline.Step, + // $HookSequence extends HookSequence, + // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, + // $Result = unknown, + // $Name extends $HookSequence[number] = $HookSequence[number], $Options extends InterceptorOptions = InterceptorOptions, -> = PublicHook< - // (<$$Input extends $HookMap[$Name]['input']>( - (( - input?: - & { - input?: $HookMap[$Name]['input'] - } - & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), - ) => InferPublicHookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>), +> = PublicStep< + ((...args: Parameters<$Step['run']>) => InferPublicHookReturn<$Step, $Options>), $HookMap[$Name]['input'] > @@ -42,10 +38,11 @@ type InferPublicHook< // dprint-ignore type InferPublicHookReturn< - $HookSequence extends HookSequence, - $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - $Result = unknown, - $Name extends $HookSequence[number] = $HookSequence[number], + $Step extends Pipeline.Step, + // $HookSequence extends HookSequence, + // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, + // $Result = unknown, + // $Name extends $HookSequence[number] = $HookSequence[number], $Options extends InterceptorOptions = InterceptorOptions, > = Promise< | ($Options['retrying'] extends true ? Error : never) @@ -72,14 +69,14 @@ const hookSymbol = Symbol(`hook`) type HookSymbol = typeof hookSymbol -export type SomePublicHookEnvelope = { - [name: string]: PublicHook +export type SomePublicStepEnvelope = { + [name: string]: PublicStep } export const createPublicHook = <$OriginalInput, $Fn extends PublicHookFn>( originalInput: $OriginalInput, fn: $Fn, -): PublicHook<$Fn> => { +): PublicStep<$Fn> => { // ): $Hook & { input: $OriginalInput } => { // @ts-expect-error fn.input = originalInput @@ -87,7 +84,7 @@ export const createPublicHook = <$OriginalInput, $Fn extends PublicHookFn>( return fn } -export type PublicHook< +export type PublicStep< $Fn extends PublicHookFn = PublicHookFn, $OriginalInput extends object = object, // Exclude[0], undefined>['input'], > = diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index ca4af60c7..cd67d48e4 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,7 +1,7 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' import type { HookName } from '../hook/definition.js' -import type { NonRetryingInterceptorInput } from '../Interceptor.js' +import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< 'ErrorGraffleInterceptorEntryHook', diff --git a/src/lib/anyware/run/runHook.ts b/src/lib/anyware/run/runHook.ts index deb93574b..0433f0fe2 100644 --- a/src/lib/anyware/run/runHook.ts +++ b/src/lib/anyware/run/runHook.ts @@ -1,9 +1,9 @@ import { Errors } from '../../errors/__.js' import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../../prelude.js' import type { HookResult, HookResultErrorAsync, Slots } from '../hook/private.js' -import { createPublicHook, type SomePublicHookEnvelope } from '../hook/public.js' -import type { InterceptorGeneric } from '../Interceptor.js' -import type { Pipeline } from '../Pipeline/Pipeline.js' +import { createPublicHook, type SomePublicStepEnvelope } from '../hook/public.js' +import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' +import type { Pipeline } from '../Pipeline/__.js' import type { ResultEnvelop } from './resultEnvelope.js' type HookDoneResolver = (input: HookResult) => void @@ -35,7 +35,7 @@ interface Input { const createExecutableChunk = <$Extension extends InterceptorGeneric>(extension: $Extension) => ({ ...extension, - currentChunk: createDeferred(), + currentChunk: createDeferred(), }) export const runHook = async ( @@ -137,14 +137,14 @@ export const runHook = async ( customSlots: customSlotsResolved, }) return extensionRetry.currentChunk.promise.then(async (envelope) => { - const envelop_ = envelope as SomePublicHookEnvelope // todo ... better way? + const envelop_ = envelope as SomePublicStepEnvelope // todo ... better way? const hook = envelop_[name] // as (params:{input:object;previous:object;using:Slots}) => if (!hook) throw new Error(`Hook not found in envelope: ${name}`) // todo use inputResolved ? const result = await hook({ ...extensionInput, input: extensionInput?.input ?? inputOriginalOrFromExtension, - }) as Promise + }) as Promise return result }) } diff --git a/src/lib/anyware/run/runPipeline.ts b/src/lib/anyware/run/runPipeline.ts index c923bdb25..2cea77779 100644 --- a/src/lib/anyware/run/runPipeline.ts +++ b/src/lib/anyware/run/runPipeline.ts @@ -2,8 +2,8 @@ import type { Errors } from '../../errors/__.js' import { ContextualError } from '../../errors/ContextualError.js' import { casesExhausted, createDeferred, debug } from '../../prelude.js' import type { HookResult, HookResultErrorAsync } from '../hook/private.js' -import type { InterceptorGeneric } from '../Interceptor.js' -import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' +import type { Pipeline } from '../Pipeline/__.js' import { createResultEnvelope } from './resultEnvelope.js' import type { ResultEnvelop } from './resultEnvelope.js' import { runHook } from './runHook.js' diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 80aa062e2..a7ffd4225 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -5,17 +5,21 @@ import { casesExhausted } from '../../prelude.js' import type { Private } from '../../private.js' import type { HookName } from '../hook/definition.js' import type { HookResultErrorExtension } from '../hook/private.js' -import type { SomePublicHookEnvelope } from '../hook/public.js' -import { createRetryingInterceptor, type Interceptor, type InterceptorInput } from '../Interceptor.js' -import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { SomePublicStepEnvelope } from '../hook/public.js' +import { + createRetryingInterceptor, + type InferInterceptorConstructor, + type InterceptorInput, +} from '../Interceptor/Interceptor.js' +import type { Pipeline } from '../Pipeline/__.js' import { getEntrypoint } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' export type Runner<$Pipeline extends Pipeline = Pipeline> = ( { initialInput, interceptors, options }: { initialInput: GetInitialPipelineInput<$Pipeline> - interceptors: Interceptor<$Pipeline>[] - retryingInterceptor?: Interceptor<$Pipeline, { retrying: true }> + interceptors: InferInterceptorConstructor<$Pipeline>[] + retryingInterceptor?: InferInterceptorConstructor<$Pipeline, { retrying: true }> options?: Options }, ) => Promise['result'] | Errors.ContextualError> @@ -67,7 +71,7 @@ type GetInitialPipelineInput<$Pipeline extends Pipeline> = Private.Get< >['hookMap'][Private.Get<$Pipeline>['hookSequence'][0]]['input'] const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: InterceptorInput) => { - const currentChunk = createDeferred() + const currentChunk = createDeferred() const body = createDeferred() const extensionRun = typeof interceptor === `function` ? interceptor : interceptor.run const retrying = typeof interceptor === `function` ? false : interceptor.retrying @@ -135,7 +139,7 @@ const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: } } -const createPassthrough = (hookName: string) => async (hookEnvelope: SomePublicHookEnvelope) => { +const createPassthrough = (hookName: string) => async (hookEnvelope: SomePublicStepEnvelope) => { const hook = hookEnvelope[hookName] if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) diff --git a/src/lib/builder/Definition.ts b/src/lib/builder/Definition.ts index 00dacd7b5..f39d4a368 100644 --- a/src/lib/builder/Definition.ts +++ b/src/lib/builder/Definition.ts @@ -1,5 +1,5 @@ import type { Simplify } from 'type-fest' -import type { AssertExtends, mergeArrayOfObjects } from '../prelude.js' +import type { AssertExtends, intersectArrayOfObjects } from '../prelude.js' import type { Private } from '../private.js' import type { TypeFunction } from '../type-function/__.js' import type { Context, Extension } from './Extension.js' @@ -55,11 +55,11 @@ export type MaterializeGeneric<$Chain_ extends Definition_> = Private.Add< { chain: $Chain_, - context: mergeArrayOfObjects< + context: intersectArrayOfObjects< MaterializeExtensionsGenericContext<$Chain_['extensions']> > }, - mergeArrayOfObjects< + intersectArrayOfObjects< MaterializeExtensionsGeneric<$Chain_, $Chain_['extensions']> > > @@ -83,11 +83,11 @@ export type MaterializeSpecific<$Chain_ extends Definition_> = Private.Add< { chain: $Chain_, - context: mergeArrayOfObjects< + context: intersectArrayOfObjects< MaterializeExtensionsInitialContext<$Chain_['extensions']> > }, - mergeArrayOfObjects< + intersectArrayOfObjects< MaterializeExtensionsInitial<$Chain_, $Chain_['extensions']> > > @@ -113,7 +113,7 @@ export type MaterializeWithNewContext<$Chain_ extends Definition_, $Context exte chain: $Chain_, context: $Context }, - mergeArrayOfObjects< + intersectArrayOfObjects< MaterializeExtensionsWithNewState< $Chain_, $Context, diff --git a/src/lib/config-manager/ConfigManager.test-d.ts b/src/lib/config-manager/ConfigManager.test-d.ts index bcd2a77c2..c3acd1248 100644 --- a/src/lib/config-manager/ConfigManager.test-d.ts +++ b/src/lib/config-manager/ConfigManager.test-d.ts @@ -30,7 +30,7 @@ assertEqual, never>() assertEqual, never>() assertEqual, { a: { b: never } }>() -assertEqual, { a: { b: 2 }; b: string }>() +assertEqual, { a: { b: 2 }; b: string }>() // dprint-ignore { diff --git a/src/lib/config-manager/ConfigManager.ts b/src/lib/config-manager/ConfigManager.ts index 4aa1a7fa5..ccf4707cb 100644 --- a/src/lib/config-manager/ConfigManager.ts +++ b/src/lib/config-manager/ConfigManager.ts @@ -1,6 +1,6 @@ -import type { IsUnknown, PartialDeep, Simplify } from 'type-fest' +import type { PartialDeep, Simplify } from 'type-fest' import { isDate } from 'util/types' -import { type ExcludeUndefined, type GuardedType, isAnyFunction, isNonNullObject } from '../prelude.js' +import { type ExcludeUndefined, type GuardedType, isAnyFunction, isNonNullObject, type OrDefault } from '../prelude.js' // dprint-ignore export type MergeDefaults<$Defaults extends object, $Input extends undefined | object, $CustomScalars> = @@ -111,14 +111,6 @@ export type AppendOptional<$Array extends any[], $Value> = $Value extends undefi export type GetAtPathOrDefault<$Obj, $Path extends Path, $Default> = OrDefault, $Default> -// dprint-ignore -export type OrDefault<$Value, $Default> = - // When no value has been passed in, because the property is optional, - // then the inferred type is unknown. - IsUnknown<$Value> extends true ? $Default : - $Value extends undefined ? $Default : - $Value - // dprint-ignore export type GetOptional<$Value, $Path extends [...string[]]> = $Value extends undefined ? undefined : @@ -146,7 +138,7 @@ export type SetMany<$Obj extends object, $Sets extends [Path, any][]> = > : never -export type SetKey<$Obj extends object, $Prop extends keyof $Obj, $Type extends $Obj[$Prop]> = +export type SetOneKey<$Obj extends object, $Prop extends keyof $Obj, $Type extends $Obj[$Prop]> = & Omit<$Obj, $Prop> & { [_ in $Prop]: $Type } diff --git a/src/lib/prelude.test-d.ts b/src/lib/prelude.test-d.ts index a856db74d..63ab858f9 100644 --- a/src/lib/prelude.test-d.ts +++ b/src/lib/prelude.test-d.ts @@ -1,5 +1,5 @@ import { assertEqual } from './assert-equal.js' -import type { OmitKeysWithPrefix, ToParameters } from './prelude.js' +import { type GetLastValue, type OmitKeysWithPrefix, type ToParameters, type Tuple } from './prelude.js' // dprint-ignore { @@ -13,4 +13,19 @@ assertEqual , []>() assertEqual , [{ a:1; b?:2 }]>() assertEqual , [{ a?:1; b?:2 }]|[]>() +assertEqual, 3>() +// @ts-expect-error +GetLastValue<[]> + +// Tuple.* + +assertEqual, [1, 2, 3]>() +assertEqual, [3]>() +assertEqual, []>() + +assertEqual, 2>() +assertEqual, undefined>() + +assertEqual, 2>() +assertEqual, false>() } diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 579b9962e..4253a8f04 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -298,7 +298,37 @@ export const debugSub = (...args: any[]) => (...subArgs: any[]) => { debug(...args, ...subArgs) } -export type PlusOneUpToTen = n extends 0 ? 1 +// dprint-ignore +export type OrDefault<$Value, $Default> = + // When no value has been passed in because the property is optional, + // then the inferred type is unknown. + IsUnknown<$Value> extends true ? $Default : + $Value extends undefined ? $Default : + $Value + +export namespace Tuple { + // dprint-ignore + export type GetAtNextIndex<$Items extends readonly any[], $Index extends NumberLiteral> = + $Items[PlusOne<$Index>] + + // dprint-ignore + export type GetNextIndexOr<$Items extends readonly any[], $Index extends number, $Or> = + OrDefault, $Or> + + // dprint-ignore + export type DropUntilIndex<$Items extends readonly any[], $Index extends NumberLiteral> = + $Index extends 0 ? $Items : + $Items extends readonly [infer _, ...infer $Rest] ? DropUntilIndex<$Rest, MinusOne<$Index>> : + [] + + export type IndexPlusOne<$Index extends NumberLiteral> = PlusOne<$Index> +} + +type NumberLiteral = number | `${number}` + +// dprint-ignore +export type PlusOne = + n extends 0 ? 1 : n extends 1 ? 2 : n extends 2 ? 3 : n extends 3 ? 4 @@ -310,7 +340,9 @@ export type PlusOneUpToTen = n extends 0 ? 1 : n extends 9 ? 10 : never -export type MinusOneUpToTen = n extends 10 ? 9 +// dprint-ignore +export type MinusOne = + n extends 10 ? 9 : n extends 9 ? 8 : n extends 8 ? 7 : n extends 7 ? 6 @@ -330,10 +362,9 @@ export type findIndexForValue = type findIndexForValue_ = value extends list[i] ? i - : findIndexForValue_> + : findIndexForValue_> -export type FindValueAfter = - list[PlusOneUpToTen>] +export type FindValueAfter = list[PlusOne>] // dprint-ignore export type TakeValuesBefore<$Value, $List extends AnyReadOnlyList> = @@ -349,11 +380,11 @@ type AnyReadOnlyList = readonly [...any[]] export type ValueOr = value extends undefined ? orValue : value export type FindValueAfterOr = ValueOr< - list[PlusOneUpToTen>], + list[PlusOne>], orValue > -export type GetLastValue = T[MinusOneUpToTen] +export type GetLastValue = T[MinusOne] export type IsLastValue = value extends GetLastValue ? true : false @@ -458,8 +489,8 @@ export const shallowMergeDefaults = <$Defaults extends object, $Input extends ob return merged as any } -export type mergeArrayOfObjects = T extends [infer $First, ...infer $Rest extends any[]] - ? $First & mergeArrayOfObjects<$Rest> +export type intersectArrayOfObjects = T extends [infer $First, ...infer $Rest extends any[]] + ? $First & intersectArrayOfObjects<$Rest> : {} export const identityProxy = new Proxy({}, { @@ -637,7 +668,7 @@ export type SimplifyExcept<$ExcludeType, $Type> = ? $Type : {[TypeKey in keyof $Type]: $Type[TypeKey]} -export const any = undefined as any +export const _ = undefined as any // dprint-ignore export type ToParameters<$Params extends object | undefined> = diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index 6a1fad08a..aa695bc21 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -1,5 +1,3 @@ -import type { GraffleExecutionResultVar } from '../client/handleOutput.js' -import type { Config } from '../client/Settings/Config.js' import { MethodMode, type MethodModeGetReads } from '../client/transportHttp/request.js' import { Anyware } from '../lib/anyware/__.js' import type { Grafaid } from '../lib/grafaid/__.js' @@ -14,36 +12,25 @@ import { } from '../lib/grafaid/http/http.js' import { normalizeRequestToNode } from '../lib/grafaid/request.js' import { mergeRequestInit, searchParamsAppendAll } from '../lib/http.js' -import { casesExhausted, isAbortError, isString } from '../lib/prelude.js' +import { casesExhausted, isString } from '../lib/prelude.js' import { Transport } from '../types/Transport.js' import { decodeResultData } from './CustomScalars/decode.js' import { encodeRequestVariables } from './CustomScalars/encode.js' -import { - type CoreExchangeGetRequest, - type CoreExchangePostRequest, - type HookMap, - hookNamesOrderedBySequence, - type HookSequence, -} from './types.js' - -export type RequestPipeline<$Config extends Config = Config> = Anyware.Pipeline< - HookSequence, - HookMap<$Config>, - GraffleExecutionResultVar<$Config> -> - -export const RequestPipeline = Anyware.Pipeline.create({ - // If core errors caused by an abort error then raise it as a direct error. - // This is an expected possible error. Possible when user cancels a request. - passthroughErrorWith: (signal) => { - // todo have anyware propagate the input that was passed to the hook that failed. - // it will give us a bit more confidence that we're only allowing this abort error for fetch requests stuff - // context.config.transport.type === Transport.http - return signal.hookName === `exchange` && isAbortError(signal.error) - }, - hookNamesOrderedBySequence, - hooks: { - encode: ({ input }) => { + +import type { FormattedExecutionResult, GraphQLSchema } from 'graphql' +import type { Context } from '../client/context.js' +import type { Config } from '../client/Settings/Config.js' +import type { MethodModePost } from '../client/transportHttp/request.js' +import type { httpMethodGet, httpMethodPost } from '../lib/http.js' +import type { TransportHttp, TransportMemory } from '../types/Transport.js' + +export type RequestPipeline = Anyware.Pipeline.Infer + +export const RequestPipeline = Anyware.Pipeline + .create() + .step({ + name: `encode`, + run: ({ input }): RequestPipeline.Hooks.HookDefPack['input'] => { const sddm = input.state.schemaMap const scalars = input.state.scalars.map if (sddm) { @@ -57,56 +44,59 @@ export const RequestPipeline = Anyware.Pipeline.create { - const graphqlRequest: Grafaid.HTTP.RequestConfig = { - operationName: input.request.operationName, - variables: input.request.variables, - query: print(input.request.query), - } + }) + .step({ + name: `pack`, + slots: { + searchParams: getRequestEncodeSearchParameters, + body: postRequestEncodeBody, + }, + run: ({ input, slots }) => { + const graphqlRequest: Grafaid.HTTP.RequestConfig = { + operationName: input.request.operationName, + variables: input.request.variables, + query: print(input.request.query), + } - // TODO thrown error here is swallowed in examples. - switch (input.transportType) { - case `memory`: { - return { - ...input, - request: graphqlRequest, - } + // TODO thrown error here is swallowed in examples. + switch (input.transportType) { + case `memory`: { + return { + ...input, + request: graphqlRequest, } - case `http`: { - if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) - - const operationType = isString(input.request.operation) - ? input.request.operation - : input.request.operation.operation - const methodMode = input.state.config.transport.config.methodMode - const requestMethod = methodMode === MethodMode.post - ? `post` - : methodMode === MethodMode.getReads - ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` - : casesExhausted(methodMode) - - const baseProperties = mergeRequestInit( + } + case `http`: { + if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) + + const operationType = isString(input.request.operation) + ? input.request.operation + : input.request.operation.operation + const methodMode = input.state.config.transport.config.methodMode + const requestMethod = methodMode === MethodMode.post + ? `post` + : methodMode === MethodMode.getReads + ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` + : casesExhausted(methodMode) + + const baseProperties = mergeRequestInit( + mergeRequestInit( mergeRequestInit( - mergeRequestInit( - { - headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec, - }, - { - headers: input.state.config.transport.config.headers, - }, - ), - input.state.config.transport.config.raw, + { + headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec, + }, + { + headers: input.state.config.transport.config.headers, + }, ), - { - headers: input.headers, - }, - ) - const request: CoreExchangePostRequest | CoreExchangeGetRequest = requestMethod === `get` + input.state.config.transport.config.raw, + ), + { + headers: input.headers, + }, + ) + const request: RequestPipeline.Hooks.CoreExchangePostRequest | RequestPipeline.Hooks.CoreExchangeGetRequest = + requestMethod === `get` ? { methodMode: methodMode as MethodModeGetReads, ...baseProperties, @@ -120,45 +110,48 @@ export const RequestPipeline = Anyware.Pipeline.create fetch(request), - }, - run: async ({ input, slots }) => { - switch (input.transportType) { - case `http`: { - const request = new Request(input.request.url, input.request) - const response = await slots.fetch(request) - return { - ...input, - response, - } + }) + .step({ + name: `exchange`, + slots: { + // Put fetch behind a lambda so that it can be easily globally overridden + // by fixtures. + fetch: (requestInfo: RequestInfo) => fetch(requestInfo), + }, + run: async ({ input, slots }) => { + switch (input.transportType) { + case `http`: { + const request = new Request(input.request.url, input.request) + const response = await slots.fetch(request) + return { + ...input, + response, } - case `memory`: { - const result = await execute(input) - return { - ...input, - result, - } + } + case `memory`: { + const result = await execute(input) + return { + ...input, + result, } - default: - throw casesExhausted(input) } - }, + default: + throw casesExhausted(input) + } }, - unpack: async ({ input }) => { + }) + .step({ + name: `unpack`, + run: async ({ input }) => { switch (input.transportType) { case `http`: { // todo 1 if response is missing header of content length then .json() hangs forever. @@ -181,7 +174,10 @@ export const RequestPipeline = Anyware.Pipeline.create { + }) + .step({ + name: `decode`, + run: ({ input, previous }) => { // If there has been an error and we definitely don't have any data, such as when // giving an operation name that doesn't match any in the document, // then don't attempt to decode. @@ -204,11 +200,119 @@ export const RequestPipeline = Anyware.Pipeline.create = + | ( + TransportHttp extends $Config['transport']['type'] + ? ({ + transportType: TransportHttp + url: string | URL + } & $HttpProperties) + : never + ) + | ( + TransportMemory extends $Config['transport']['type'] + ? ({ + transportType: TransportMemory + schema: GraphQLSchema + } & $MemoryProperties) + : never + ) + + // --------------------------- + + export type HookDefEncode<$Config extends Config = Config> = { + input: + & { request: Grafaid.RequestAnalyzedInput } + & HookInputBase + & TransportInput<$Config> + } + + export type HookDefPack<$Config extends Config = Config> = { + input: + & HookInputBase + & TransportInput< + $Config, + // todo why is headers here but not other http request properties? + { headers?: HeadersInit } + > + & { request: Grafaid.RequestAnalyzedInput } + slots: { + /** + * When request will be sent using GET this slot is called to create the value that will be used for the HTTP Search Parameters. + */ + searchParams: getRequestEncodeSearchParameters + /** + * When request will be sent using POST this slot is called to create the value that will be used for the HTTP body. + */ + body: postRequestEncodeBody + } + } + + export type HookDefExchange<$Config extends Config> = { + slots: { + fetch: (request: Request) => Response | Promise + } + input: + & HookInputBase + & TransportInput< + $Config, + { request: CoreExchangePostRequest | CoreExchangeGetRequest; headers?: HeadersInit }, + { request: Grafaid.HTTP.RequestConfig } + > + } + + // export type HookDefUnpack<$Config extends Config> = { + // input: + // & HookInputBase + // & TransportInput< + // $Config, + // { response: Response }, + // { result: FormattedExecutionResult } + // > + // } + + // export type HookDefDecode<$Config extends Config> = { + // input: + // & HookInputBase + // & TransportInput< + // $Config, + // { response: Response } + // > + // & { result: FormattedExecutionResult } + // } + + // export type HookMap<$Config extends Config = Config> = { + // encode: HookDefEncode<$Config> + // pack: HookDefPack<$Config> + // exchange: HookDefExchange<$Config> + // unpack: HookDefUnpack<$Config> + // decode: HookDefDecode<$Config> + // } + + /** + * An extension of {@link RequestInit} that adds a required `url` property and makes `body` required. + */ + export type CoreExchangePostRequest = Omit & { + methodMode: MethodModePost | MethodModeGetReads + method: httpMethodPost + url: string | URL // todo URL for config and string only for input. Requires anyware to allow different types for input and existing config. + body: BodyInit + } + + export type CoreExchangeGetRequest = Omit & { + methodMode: MethodModeGetReads + method: httpMethodGet + url: string | URL + } + } +} diff --git a/src/requestPipeline/_.ts b/src/requestPipeline/_.ts deleted file mode 100644 index 4ae5b6598..000000000 --- a/src/requestPipeline/_.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './RequestPipeline.js' -export * as Hooks from './types.js' diff --git a/src/requestPipeline/__.ts b/src/requestPipeline/__.ts index 35dd2529c..86baf9a24 100644 --- a/src/requestPipeline/__.ts +++ b/src/requestPipeline/__.ts @@ -1 +1 @@ -export * as RequestPipeline from './_.js' +export * from './RequestPipeline.js' diff --git a/src/requestPipeline/types.ts b/src/requestPipeline/types.ts deleted file mode 100644 index 080e9e2c1..000000000 --- a/src/requestPipeline/types.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { FormattedExecutionResult, GraphQLSchema } from 'graphql' -import type { Context } from '../client/context.js' -import type { Config } from '../client/Settings/Config.js' -import type { MethodModeGetReads, MethodModePost } from '../client/transportHttp/request.js' -import type { Grafaid } from '../lib/grafaid/__.js' -import type { getRequestEncodeSearchParameters, postRequestEncodeBody } from '../lib/grafaid/http/http.js' -import type { httpMethodGet, httpMethodPost } from '../lib/http.js' -import type { TransportHttp, TransportMemory } from '../types/Transport.js' - -interface HookInputBase { - state: Context -} - -// dprint-ignore - -type TransportInput<$Config extends Config, $HttpProperties = {}, $MemoryProperties = {}> = - | ( - TransportHttp extends $Config['transport']['type'] - ? ({ - transportType: TransportHttp - url: string | URL - } & $HttpProperties) - : never - ) - | ( - TransportMemory extends $Config['transport']['type'] - ? ({ - transportType: TransportMemory - schema: GraphQLSchema - } & $MemoryProperties) - : never - ) - -// --------------------------- - -export type HookDefEncode<$Config extends Config> = { - input: - & { request: Grafaid.RequestAnalyzedInput } - & HookInputBase - & TransportInput<$Config> -} - -export type HookDefPack<$Config extends Config> = { - input: - & HookInputBase - & TransportInput< - $Config, - // todo why is headers here but not other http request properties? - { headers?: HeadersInit } - > - & { request: Grafaid.RequestAnalyzedInput } - slots: { - /** - * When request will be sent using GET this slot is called to create the value that will be used for the HTTP Search Parameters. - */ - searchParams: getRequestEncodeSearchParameters - /** - * When request will be sent using POST this slot is called to create the value that will be used for the HTTP body. - */ - body: postRequestEncodeBody - } -} - -export type HookDefExchange<$Config extends Config> = { - slots: { - fetch: (request: Request) => Response | Promise - } - input: - & HookInputBase - & TransportInput< - $Config, - { request: CoreExchangePostRequest | CoreExchangeGetRequest; headers?: HeadersInit }, - { request: Grafaid.HTTP.RequestConfig } - > -} - -export type HookDefUnpack<$Config extends Config> = { - input: - & HookInputBase - & TransportInput< - $Config, - { response: Response }, - { result: FormattedExecutionResult } - > -} - -export type HookDefDecode<$Config extends Config> = { - input: - & HookInputBase - & TransportInput< - $Config, - { response: Response } - > - & { result: FormattedExecutionResult } -} - -export type HookMap<$Config extends Config = Config> = { - encode: HookDefEncode<$Config> - pack: HookDefPack<$Config> - exchange: HookDefExchange<$Config> - unpack: HookDefUnpack<$Config> - decode: HookDefDecode<$Config> -} - -export const hookNamesOrderedBySequence = [`encode`, `pack`, `exchange`, `unpack`, `decode`] as const - -export type HookSequence = typeof hookNamesOrderedBySequence - -/** - * An extension of {@link RequestInit} that adds a required `url` property and makes `body` required. - */ -export type CoreExchangePostRequest = Omit & { - methodMode: MethodModePost | MethodModeGetReads - method: httpMethodPost - url: string | URL // todo URL for config and string only for input. Requires anyware to allow different types for input and existing config. - body: BodyInit -} - -export type CoreExchangeGetRequest = Omit & { - methodMode: MethodModeGetReads - method: httpMethodGet - url: string | URL -} From 19d251d1dedbd1de171f30e1f840860111f9fbdf Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 14:10:45 -0500 Subject: [PATCH 02/36] awaited --- src/lib/anyware/Pipeline/builder.test-d.ts | 5 +++++ src/lib/anyware/Pipeline/builder.ts | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/Pipeline/builder.test-d.ts index 5746e902b..f08b496d5 100644 --- a/src/lib/anyware/Pipeline/builder.test-d.ts +++ b/src/lib/anyware/Pipeline/builder.test-d.ts @@ -37,6 +37,11 @@ test(`second step definition`, () => { } >() }) +test(`step input receives awaited return value from previous step `, () => { + const p1 = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) + type s2Parameters = Parameters[0]['run']>[0]['input'] + expectTypeOf().toEqualTypeOf() +}) test(`step definition with slots`, () => { const p1 = p0 diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index f6f8cf08c..cacebe10d 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -75,7 +75,7 @@ type GetNextStepPrevious_<$Steps extends Step[]> = intersectArrayOfObjects< // dprint-ignore type GetNextStepParameterInput<$Pipeline extends Pipeline> = $Pipeline['steps'] extends [any, ...any[]] - ? Step.GetResult> + ? Awaited>> : $Pipeline['input'] export interface Builder<$Context extends Context = Context> { @@ -121,8 +121,6 @@ export interface Builder<$Context extends Context = Context> { > } -// export type Pipeline = Context - export type Infer<$Builder extends Builder> = $Builder['context'] /** From fd78523f4aff79820d2808d08c31b934970d2717 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 14:22:58 -0500 Subject: [PATCH 03/36] optional trigger arguments --- src/extensions/Upload/Upload.ts | 67 ++++++++++--------- .../anyware/Interceptor/Interceptor.test-d.ts | 11 ++- src/lib/anyware/Interceptor/Interceptor.ts | 28 ++++---- 3 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/extensions/Upload/Upload.ts b/src/extensions/Upload/Upload.ts index 1b50f23b1..0f6d2c555 100644 --- a/src/extensions/Upload/Upload.ts +++ b/src/extensions/Upload/Upload.ts @@ -10,41 +10,44 @@ export const Upload = createExtension({ create: () => { return { onRequest: async ({ pack }) => { - // TODO we can probably get file upload working for in-memory schemas too :) - if (pack.input.transportType !== `http`) { - throw new Error(`Must be using http transport to use "Upload" scalar.`) - } + return pack() + }, + // onRequest: async ({ pack }) => { + // // TODO we can probably get file upload working for in-memory schemas too :) + // if (pack.input.transportType !== `http`) { + // throw new Error(`Must be using http transport to use "Upload" scalar.`) + // } - // Remove the content-type header so that fetch sets it automatically upon seeing the body is a FormData instance. - // @see https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/ - // @see https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data - // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition - return await pack({ - // todo rename "using" to "with" - using: { - body: (input) => { - const hasUploadScalarVariable = input.variables && isUsingUploadScalar(input.variables) - if (!hasUploadScalarVariable) return + // // Remove the content-type header so that fetch sets it automatically upon seeing the body is a FormData instance. + // // @see https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/ + // // @see https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data + // // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + // return await pack({ + // // todo rename "using" to "with" + // using: { + // body: (input) => { + // const hasUploadScalarVariable = input.variables && isUsingUploadScalar(input.variables) + // if (!hasUploadScalarVariable) return - // TODO we can probably get file upload working for in-memory schemas too :) - if (pack.input.transportType !== `http`) { - throw new Error(`Must be using http transport to use "Upload" scalar.`) - } + // // TODO we can probably get file upload working for in-memory schemas too :) + // if (pack.input.transportType !== `http`) { + // throw new Error(`Must be using http transport to use "Upload" scalar.`) + // } - return createBody({ - query: input.query, - variables: input.variables!, - }) - }, - }, - input: { - ...pack.input, - headers: { - 'content-type': ``, - }, - }, - }) - }, + // return createBody({ + // query: input.query, + // variables: input.variables!, + // }) + // }, + // }, + // input: { + // ...pack.input, + // headers: { + // 'content-type': ``, + // }, + // }, + // }) + // }, } }, }) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 9332842fb..fce233afc 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -24,6 +24,13 @@ describe(`interceptor constructor`, () => { }]>() }) + // --- trigger --- + test(`trigger arguments are optional`, () => { + const p = Pipeline.create().step({ name: `a`, run: () => results.a }) + type i = Interceptor.InferConstructor + expectTypeOf<[]>().toMatchTypeOf[0]['a']>>() + }) + test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { const p = Pipeline.create().step({ name: `a`, @@ -33,9 +40,9 @@ describe(`interceptor constructor`, () => { .step({ name: `b`, run: () => results.b }) type i = Interceptor.InferConstructor type stepAParameters = Parameters[0]['a']> - expectTypeOf().toEqualTypeOf<[params: { input?: initialInput; slots?: { m?: slots['m'] } }]> + expectTypeOf().toEqualTypeOf<[params?: { input?: initialInput; slots?: { m?: slots['m'] } }]> type stepBParameters = Parameters[0]['b']> - expectTypeOf().toEqualTypeOf<[params: { input?: results['a'] }]> // no "slots" key! + expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "slots" key! }) // --- return --- diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index 72beeba46..50acda0c5 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,7 +1,6 @@ import type { Simplify } from 'type-fest' -import type { Deferred, intersectArrayOfObjects, MaybePromise, Tuple } from '../../prelude.js' -import type { Private } from '../../private.js' -import type { InferPublicHooks, SomePublicStepEnvelope } from '../hook/public.js' +import type { Deferred, MaybePromise } from '../../prelude.js' +import type { SomePublicStepEnvelope } from '../hook/public.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Pipeline/builder.js' @@ -18,15 +17,17 @@ export interface Interceptor { } export namespace Interceptor { - export type InferConstructor< + export interface InferConstructor< $Pipeline extends Pipeline = Pipeline, > // $Options extends InterceptorOptions = InterceptorOptions, - = ( - steps: Simplify>, - ) => Promise< - | Pipeline.GetAwaitedResult<$Pipeline> - | SomePublicStepEnvelope - > + { + ( + steps: Simplify>, + ): Promise< + | Pipeline.GetAwaitedResult<$Pipeline> + | SomePublicStepEnvelope + > + } type InferConstructorKeywordArguments< $Pipeline extends Pipeline, @@ -45,9 +46,9 @@ export namespace Interceptor { : {} // dprint-ignore - type InferStepTrigger<$Step extends Step, $NextSteps extends Step[], $PipelineOutput> = + interface InferStepTrigger<$Step extends Step, $NextSteps extends Step[], $PipelineOutput> { ( - params: Simplify< + params?: Simplify< & { input?: $Step['input'] } @@ -57,13 +58,14 @@ export namespace Interceptor { : { slots?: Partial<$Step['slots']> } ) > - ) => Promise< + ): Promise< $NextSteps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] ? { [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> } : $PipelineOutput > + } } export type InterceptorGeneric = NonRetryingInterceptor | RetryingInterceptor From 9f354da84ef9c5f95d47049dfd96349399b55d58 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 14:28:58 -0500 Subject: [PATCH 04/36] trigger input type --- src/extensions/Upload/Upload.ts | 70 +++++++++---------- .../anyware/Interceptor/Interceptor.test-d.ts | 16 +++-- src/lib/anyware/Interceptor/Interceptor.ts | 13 ++-- 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/extensions/Upload/Upload.ts b/src/extensions/Upload/Upload.ts index 0f6d2c555..1e9f3af9e 100644 --- a/src/extensions/Upload/Upload.ts +++ b/src/extensions/Upload/Upload.ts @@ -9,45 +9,45 @@ export const Upload = createExtension({ name: `Upload`, create: () => { return { - onRequest: async ({ pack }) => { - return pack() - }, // onRequest: async ({ pack }) => { - // // TODO we can probably get file upload working for in-memory schemas too :) - // if (pack.input.transportType !== `http`) { - // throw new Error(`Must be using http transport to use "Upload" scalar.`) - // } + // return pack() + // }, + onRequest: async ({ pack }) => { + // TODO we can probably get file upload working for in-memory schemas too :) + if (pack.input.transportType !== `http`) { + throw new Error(`Must be using http transport to use "Upload" scalar.`) + } - // // Remove the content-type header so that fetch sets it automatically upon seeing the body is a FormData instance. - // // @see https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/ - // // @see https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data - // // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition - // return await pack({ - // // todo rename "using" to "with" - // using: { - // body: (input) => { - // const hasUploadScalarVariable = input.variables && isUsingUploadScalar(input.variables) - // if (!hasUploadScalarVariable) return + // Remove the content-type header so that fetch sets it automatically upon seeing the body is a FormData instance. + // @see https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/ + // @see https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data + // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + return await pack({ + // todo rename "using" to "with" + using: { + body: (input) => { + const hasUploadScalarVariable = input.variables && isUsingUploadScalar(input.variables) + if (!hasUploadScalarVariable) return - // // TODO we can probably get file upload working for in-memory schemas too :) - // if (pack.input.transportType !== `http`) { - // throw new Error(`Must be using http transport to use "Upload" scalar.`) - // } + // TODO we can probably get file upload working for in-memory schemas too :) + if (pack.input.transportType !== `http`) { + throw new Error(`Must be using http transport to use "Upload" scalar.`) + } - // return createBody({ - // query: input.query, - // variables: input.variables!, - // }) - // }, - // }, - // input: { - // ...pack.input, - // headers: { - // 'content-type': ``, - // }, - // }, - // }) - // }, + return createBody({ + query: input.query, + variables: input.variables!, + }) + }, + }, + input: { + ...pack.input, + headers: { + 'content-type': ``, + }, + }, + }) + }, } }, }) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index fce233afc..dbc91791f 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -25,6 +25,14 @@ describe(`interceptor constructor`, () => { }) // --- trigger --- + + test(`original input on self`, () => { + const p = Pipeline.create().step({ name: `a`, run: () => results.a }) + type i = Interceptor.InferConstructor + type triggerA = Parameters[0]['a'] + expectTypeOf().toMatchTypeOf() + }) + test(`trigger arguments are optional`, () => { const p = Pipeline.create().step({ name: `a`, run: () => results.a }) type i = Interceptor.InferConstructor @@ -39,10 +47,10 @@ describe(`interceptor constructor`, () => { }) .step({ name: `b`, run: () => results.b }) type i = Interceptor.InferConstructor - type stepAParameters = Parameters[0]['a']> - expectTypeOf().toEqualTypeOf<[params?: { input?: initialInput; slots?: { m?: slots['m'] } }]> - type stepBParameters = Parameters[0]['b']> - expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "slots" key! + type triggerAParameters = Parameters[0]['a']> + expectTypeOf().toEqualTypeOf<[params?: { input?: initialInput; slots?: { m?: slots['m'] } }]> + type triggerBParameters = Parameters[0]['b']> + expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "slots" key! }) // --- return --- diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index 50acda0c5..a90bc86e2 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -59,12 +59,13 @@ export namespace Interceptor { ) > ): Promise< - $NextSteps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] - ? { - [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> - } - : $PipelineOutput - > + $NextSteps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] + ? { + [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> + } + : $PipelineOutput + > + input: $Step['input'] } } From 793ba658e6622aafbd39788f7a46ff0f8ad386d8 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 14:30:06 -0500 Subject: [PATCH 05/36] rename trigger slots to using --- src/lib/anyware/Interceptor/Interceptor.test-d.ts | 4 ++-- src/lib/anyware/Interceptor/Interceptor.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index dbc91791f..29f4ea998 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -48,9 +48,9 @@ describe(`interceptor constructor`, () => { .step({ name: `b`, run: () => results.b }) type i = Interceptor.InferConstructor type triggerAParameters = Parameters[0]['a']> - expectTypeOf().toEqualTypeOf<[params?: { input?: initialInput; slots?: { m?: slots['m'] } }]> + expectTypeOf().toEqualTypeOf<[params?: { input?: initialInput; using?: { m?: slots['m'] } }]> type triggerBParameters = Parameters[0]['b']> - expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "slots" key! + expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "using" key! }) // --- return --- diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index a90bc86e2..a946e0c36 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -55,7 +55,7 @@ export namespace Interceptor { & ( $Step['slots'] extends undefined ? {} - : { slots?: Partial<$Step['slots']> } + : { using?: Partial<$Step['slots']> } ) > ): Promise< From 4b113b61cbfc26d58473dc190289eabe3f37bb8f Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 14:31:22 -0500 Subject: [PATCH 06/36] no todo --- src/extensions/Upload/Upload.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extensions/Upload/Upload.ts b/src/extensions/Upload/Upload.ts index 1e9f3af9e..1dc30b78f 100644 --- a/src/extensions/Upload/Upload.ts +++ b/src/extensions/Upload/Upload.ts @@ -23,7 +23,6 @@ export const Upload = createExtension({ // @see https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition return await pack({ - // todo rename "using" to "with" using: { body: (input) => { const hasUploadScalarVariable = input.variables && isUsingUploadScalar(input.variables) From 23f7e1d4f90722ea9ddfc4e40f3134a0d0f83d18 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 14:59:44 -0500 Subject: [PATCH 07/36] optional slots --- .../anyware/Interceptor/Interceptor.test-d.ts | 40 +++++++++++++++---- src/lib/anyware/Interceptor/Interceptor.ts | 7 +++- src/lib/anyware/__.test-helpers.ts | 2 +- src/lib/prelude.ts | 10 +++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 29f4ea998..00f73d134 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -1,11 +1,13 @@ import { describe, expectTypeOf, test } from 'vitest' -import { _ } from '../../prelude.js' +import { _, type ExcludeUndefined } from '../../prelude.js' import { type Interceptor, Pipeline } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import type { SomePublicStepEnvelope } from '../hook/public.js' -const p1 = Pipeline.create() +const p0 = Pipeline.create() + +const p1 = p0 .step({ name: `a`, run: () => results.a }) .step({ name: `b`, run: () => results.b }) .step({ name: `c`, run: () => results.c }) @@ -27,20 +29,20 @@ describe(`interceptor constructor`, () => { // --- trigger --- test(`original input on self`, () => { - const p = Pipeline.create().step({ name: `a`, run: () => results.a }) + const p = p0.step({ name: `a`, run: () => results.a }) type i = Interceptor.InferConstructor type triggerA = Parameters[0]['a'] expectTypeOf().toMatchTypeOf() }) test(`trigger arguments are optional`, () => { - const p = Pipeline.create().step({ name: `a`, run: () => results.a }) + const p = p0.step({ name: `a`, run: () => results.a }) type i = Interceptor.InferConstructor expectTypeOf<[]>().toMatchTypeOf[0]['a']>>() }) test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { - const p = Pipeline.create().step({ + const p = p0.step({ name: `a`, slots: { m: slots.m }, run: () => Promise.resolve(results.a), @@ -48,11 +50,35 @@ describe(`interceptor constructor`, () => { .step({ name: `b`, run: () => results.b }) type i = Interceptor.InferConstructor type triggerAParameters = Parameters[0]['a']> - expectTypeOf().toEqualTypeOf<[params?: { input?: initialInput; using?: { m?: slots['m'] } }]> + expectTypeOf().toEqualTypeOf< + [params?: { input?: initialInput; using?: { m?: () => Promise<'m' | undefined> } }] + > type triggerBParameters = Parameters[0]['b']> expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "using" key! }) + // --- slots --- + + test(`slots are optional`, () => { + const p = p0.step({ name: `a`, slots, run: () => results.a }) + type triggerA = Parameters>[0]['a'] + type triggerASlotInputs = ExcludeUndefined[0]>['using']> + expectTypeOf<{ m?: any; n?: any }>().toMatchTypeOf() + }) + + test(`slot function can return undefined (falls back to default slot)`, () => { + const p = p0.step({ name: `a`, slots, run: () => results.a }) + type triggerA = Parameters>[0]['a'] + type triggerASlotMOutput = ReturnType< + ExcludeUndefined[0]>['using']>['m']> + > + expectTypeOf>().toEqualTypeOf() + type triggerASlotNOutput = ReturnType< + ExcludeUndefined[0]>['using']>['n']> + > + expectTypeOf<`n` | undefined>().toEqualTypeOf() + }) + // --- return --- test(`can return pipeline output or a step envelope`, () => { @@ -60,7 +86,7 @@ describe(`interceptor constructor`, () => { }) test(`return type awaits pipeline output`, () => { - const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(results.a) }) + const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) type i = Interceptor.InferConstructor expectTypeOf>().toEqualTypeOf>() }) diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index a946e0c36..1693a0f4b 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,5 +1,5 @@ import type { Simplify } from 'type-fest' -import type { Deferred, MaybePromise } from '../../prelude.js' +import type { Deferred, Func, MaybePromise } from '../../prelude.js' import type { SomePublicStepEnvelope } from '../hook/public.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Pipeline/builder.js' @@ -55,7 +55,10 @@ export namespace Interceptor { & ( $Step['slots'] extends undefined ? {} - : { using?: Partial<$Step['slots']> } + : { using?: { + [$SlotName in keyof $Step['slots']]?: Func.AppendAwaitedReturnType<$Step['slots'][$SlotName], undefined> + } + } ) > ): Promise< diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index 941b9dab5..b92d6d78f 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -17,7 +17,7 @@ export type results = typeof results export const slots = { m: () => Promise.resolve(`m` as const), n: () => `n` as const, -} as const +} export type slots = typeof slots // type PrivateHookRunnerInput = { diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 4253a8f04..d7fc2d101 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -692,3 +692,13 @@ export const isAbortError = (error: any): error is DOMException & { name: 'Abort // todo look for an open issue with JSDOM to link here, is this just artifact of JSDOM or is it a real issue that happens in browsers? || (error instanceof Error && error.message.startsWith(`AbortError:`)) } + +export namespace Func { + // dprint-ignore + export type AppendAwaitedReturnType<$F, $ReturnTypeToAdd> = + $F extends (...args: infer $Args) => infer $Output + ? $Output extends Promise + ? (...args: $Args) => Promise | $ReturnTypeToAdd> + : (...args: $Args) => $Output | $ReturnTypeToAdd + : never +} From 17d845294f4579bb3e9059e9aec74cfa7a91d391 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 15:14:05 -0500 Subject: [PATCH 08/36] slot inputs can return undefined --- .../anyware/Interceptor/Interceptor.test-d.ts | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 00f73d134..7f2eda34f 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -4,20 +4,17 @@ import { type Interceptor, Pipeline } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import type { SomePublicStepEnvelope } from '../hook/public.js' +import type { Builder } from '../Pipeline/builder.js' const p0 = Pipeline.create() -const p1 = p0 - .step({ name: `a`, run: () => results.a }) - .step({ name: `b`, run: () => results.b }) - .step({ name: `c`, run: () => results.c }) - -type p1 = typeof p1 - describe(`interceptor constructor`, () => { - type i1 = Interceptor.InferConstructor - test(`receives keyword arguments, a step trigger for each step`, () => { + const p1 = p0 + .step({ name: `a`, run: () => results.a }) + .step({ name: `b`, run: () => results.b }) + .step({ name: `c`, run: () => results.c }) + type i1 = Interceptor.InferConstructor expectTypeOf>().toMatchTypeOf<[steps: { a: any; b: any; c: any }]>() expectTypeOf>().toMatchTypeOf<[steps: { a: (params: { input?: initialInput }) => Promise<{ b: (params: { input?: results['a'] }) => any }> @@ -30,45 +27,42 @@ describe(`interceptor constructor`, () => { test(`original input on self`, () => { const p = p0.step({ name: `a`, run: () => results.a }) - type i = Interceptor.InferConstructor - type triggerA = Parameters[0]['a'] + type triggerA = GetTriggerFromBuilder expectTypeOf().toMatchTypeOf() }) test(`trigger arguments are optional`, () => { const p = p0.step({ name: `a`, run: () => results.a }) - type i = Interceptor.InferConstructor - expectTypeOf<[]>().toMatchTypeOf[0]['a']>>() + type triggerA = GetTriggerFromBuilder + expectTypeOf<[]>().toMatchTypeOf>() }) + // --- slots --- + test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { - const p = p0.step({ - name: `a`, - slots: { m: slots.m }, - run: () => Promise.resolve(results.a), - }) - .step({ name: `b`, run: () => results.b }) - type i = Interceptor.InferConstructor - type triggerAParameters = Parameters[0]['a']> - expectTypeOf().toEqualTypeOf< - [params?: { input?: initialInput; using?: { m?: () => Promise<'m' | undefined> } }] - > - type triggerBParameters = Parameters[0]['b']> - expectTypeOf().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "using" key! + const p = p0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }) + type triggerA = GetTriggerFromBuilder + type triggerB = GetTriggerFromBuilder + expectTypeOf>().toEqualTypeOf<[params?: { + input?: initialInput + using?: { + m?: () => Promise<'m' | undefined> + n?: () => 'n' | undefined + } + }]> + expectTypeOf>().toEqualTypeOf<[params?: { input?: results['a'] }]> // no "using" key! }) - // --- slots --- - test(`slots are optional`, () => { const p = p0.step({ name: `a`, slots, run: () => results.a }) - type triggerA = Parameters>[0]['a'] + type triggerA = GetTriggerFromBuilder type triggerASlotInputs = ExcludeUndefined[0]>['using']> expectTypeOf<{ m?: any; n?: any }>().toMatchTypeOf() }) test(`slot function can return undefined (falls back to default slot)`, () => { const p = p0.step({ name: `a`, slots, run: () => results.a }) - type triggerA = Parameters>[0]['a'] + type triggerA = GetTriggerFromBuilder type triggerASlotMOutput = ReturnType< ExcludeUndefined[0]>['using']>['m']> > @@ -79,15 +73,23 @@ describe(`interceptor constructor`, () => { expectTypeOf<`n` | undefined>().toEqualTypeOf() }) - // --- return --- - + // --- output --- + // test(`can return pipeline output or a step envelope`, () => { - expectTypeOf>().toEqualTypeOf>() + const p = p0.step({ name: `a`, run: () => results.a }) + expectTypeOf>().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) - type i = Interceptor.InferConstructor - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) + +// --- Helpers --- + +// dprint-ignore +// @ts-expect-error +type GetTriggerFromBuilder<$Builder extends Builder, $TriggerName extends string> = Parameters>[0][$TriggerName] +// dprint-ignore +type GetReturnTypeFromBuilder<$Builder extends Builder> = ReturnType> From a940641f57f3fcb4ac43c9ae688ef88a04924999 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 15:17:46 -0500 Subject: [PATCH 09/36] tidy --- src/extensions/Upload/Upload.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/extensions/Upload/Upload.ts b/src/extensions/Upload/Upload.ts index 1dc30b78f..c16c7f3e0 100644 --- a/src/extensions/Upload/Upload.ts +++ b/src/extensions/Upload/Upload.ts @@ -9,9 +9,6 @@ export const Upload = createExtension({ name: `Upload`, create: () => { return { - // onRequest: async ({ pack }) => { - // return pack() - // }, onRequest: async ({ pack }) => { // TODO we can probably get file upload working for in-memory schemas too :) if (pack.input.transportType !== `http`) { From 7189601fc7d028d78700c6212b155414ed7127c6 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 5 Nov 2024 16:25:00 -0500 Subject: [PATCH 10/36] fixing type errors --- src/client/builderExtensions/anyware.ts | 8 +- src/lib/anyware/Pipeline/run.ts | 6 +- src/lib/anyware/__.entrypoint.test.ts | 19 ++- src/lib/anyware/__.test-helpers.ts | 146 +++++++++--------------- src/lib/anyware/run/runner.ts | 10 +- src/lib/config-manager/ConfigManager.ts | 12 +- src/lib/prelude.ts | 11 +- tests/_/SpyExtension.ts | 8 +- 8 files changed, 98 insertions(+), 122 deletions(-) diff --git a/src/client/builderExtensions/anyware.ts b/src/client/builderExtensions/anyware.ts index 79196b61d..b025f2193 100644 --- a/src/client/builderExtensions/anyware.ts +++ b/src/client/builderExtensions/anyware.ts @@ -1,5 +1,5 @@ import { createExtension } from '../../extension/extension.js' -import type { Anyware, Anyware as AnywareLib } from '../../lib/anyware/__.js' +import type { Anyware as AnywareLib } from '../../lib/anyware/__.js' import { Builder } from '../../lib/builder/__.js' import type { RequestPipeline } from '../../requestPipeline/__.js' import { type Context } from '../context.js' @@ -15,15 +15,15 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters + interceptor: AnywareLib.Interceptor.InferConstructor< + RequestPipeline<$Arguments['context']['config']> >, ) => Builder.Definition.MaterializeWithNewContext<$Arguments['chain'], $Arguments['context']> } export const builderExtensionAnyware = Builder.Extension.create((builder, context) => { const properties = { - anyware: (interceptor: Anyware.InferInterceptorConstructor) => { + anyware: (interceptor: AnywareLib.Interceptor.InferConstructor) => { return builder({ ...context, extensions: [ diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts index 9aec1ed99..96b46913d 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/Pipeline/run.ts @@ -1,3 +1,4 @@ +import type { Errors } from '../../errors/__.js' import type { Interceptor } from '../Interceptor/Interceptor.js' import type { Pipeline } from './__.js' import type { Builder } from './builder.js' @@ -13,7 +14,10 @@ type Run = < >( pipeline: $Builder, params?: $Params, -) => Pipeline.GetResult<$Builder['context']> +) => Promise< + | Errors.ContextualAggregateError + | Awaited> +> /** * todo diff --git a/src/lib/anyware/__.entrypoint.test.ts b/src/lib/anyware/__.entrypoint.test.ts index 46f78619f..8babc19e9 100644 --- a/src/lib/anyware/__.entrypoint.test.ts +++ b/src/lib/anyware/__.entrypoint.test.ts @@ -2,11 +2,21 @@ import { describe, expect, test } from 'vitest' import type { ContextualAggregateError } from '../errors/ContextualAggregateError.js' -import { run } from './__.test-helpers.js' +import { _ } from '../prelude.js' +import { type Interceptor, Pipeline } from './_.js' +import { initialInput } from './__.test-helpers.js' + +const run = async (interceptor: (...args: any[]) => any) => { + const pipeline = Pipeline.create() + return Pipeline.run(pipeline, { + initialInput, + interceptors: [interceptor], + }) +} describe(`invalid destructuring cases`, () => { test(`noParameters`, async () => { - const result = await run(() => 1) as ContextualAggregateError + const result = await run(() => _) as ContextualAggregateError expect({ result, errors: result.errors, @@ -24,7 +34,9 @@ describe(`invalid destructuring cases`, () => { `) }) test(`destructuredWithoutEntryHook`, async () => { - const result = await run(async ({ x }) => {}) as ContextualAggregateError + const result = await run(async ({ x }) => { + return _ + }) as ContextualAggregateError expect({ result, errors: result.errors, @@ -44,7 +56,6 @@ describe(`invalid destructuring cases`, () => { ) }) test(`multipleParameters`, async () => { - // @ts-expect-error two parameters is invalid const result = await run(async ({ x }, y) => {}) as ContextualAggregateError expect({ result, diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index b92d6d78f..e9bf3954a 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -1,6 +1,6 @@ -import type { Mock } from 'vitest' import { beforeEach, vi } from 'vitest' -import { Anyware } from './__.js' +import { Pipeline } from './_.js' +import type { Anyware } from './__.js' import type { InterceptorInput } from './Interceptor/Interceptor.js' import type { Options } from './run/runner.js' @@ -20,98 +20,62 @@ export const slots = { } export type slots = typeof slots -// type PrivateHookRunnerInput = { -// input: { value: string } -// slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } -// previous: object -// } - -// type PrivateHookRunner = (input: PrivateHookRunnerInput) => any - -// export const initialInput: PrivateHookRunnerInput['input'] = { value: `initial` } - -// type $Core = ReturnType & { -// hooks: { -// a: { -// run: Mock -// slots: { -// append: Mock<(hookName: string) => string> -// appendExtra: Mock<(hookName: string) => string> -// } -// } -// b: { -// run: Mock -// slots: { -// append: Mock<(hookName: string) => string> -// appendExtra: Mock<(hookName: string) => string> -// } -// } -// } -// } - -// export const createHook = <$Slots extends object, $Input extends object, $Result = unknown>( -// $Hook: { -// slots: $Slots -// run: (input: { input: $Input; slots: $Slots }) => $Result -// }, -// ) => $Hook - -// export const createAnyware = () => { -// const a = createHook({ -// slots: { -// append: vi.fn().mockImplementation((hookName: string) => { -// return hookName -// }), -// appendExtra: vi.fn().mockImplementation(() => { -// return `` -// }), -// }, -// run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { -// const extra = slots.appendExtra(`a`) -// return { value: input.value + `+` + slots.append(`a`) + extra } -// }), -// }) -// const b = createHook({ -// slots: { -// append: vi.fn().mockImplementation((hookName: string) => { -// return hookName -// }), -// appendExtra: vi.fn().mockImplementation(() => { -// return `` -// }), -// }, -// run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { -// const extra = slots.appendExtra(`b`) -// return { value: input.value + `+` + slots.append(`b`) + extra } -// }), -// }) +type PrivateHookRunnerInput = { + input: { value: string } + slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } + previous: object +} -// return Anyware.create<['a', 'b'], Anyware.HookDefinitionMap<['a', 'b']>, PrivateHookRunnerInput>({ -// hookNamesOrderedBySequence: [`a`, `b`], -// hooks: { a, b }, -// }) -// } +export const createPipeline = () => { + return Pipeline.create() + .step({ + name: `a`, + slots: { + append: vi.fn().mockImplementation((hookName: string) => { + return hookName + }), + appendExtra: vi.fn().mockImplementation(() => { + return `` + }), + }, + run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { + const extra = slots.appendExtra(`a`) + return { value: input.value + `+` + slots.append(`a`) + extra } + }), + }) + .step({ + name: `b`, + slots: { + append: vi.fn().mockImplementation((hookName: string) => { + return hookName + }), + appendExtra: vi.fn().mockImplementation(() => { + return `` + }), + }, + run: vi.fn().mockImplementation(({ input, slots }: PrivateHookRunnerInput) => { + const extra = slots.appendExtra(`b`) + return { value: input.value + `+` + slots.append(`b`) + extra } + }), + }) +} -// // @ts-expect-error -// export let anyware: Anyware.Builder<$Core> = null -// export let core: $Core +// @ts-expect-error +export let builder: Anyware.Builder<$Core> = null -// beforeEach(() => { -// // @ts-expect-error mock types not tracked by Anyware -// anyware = createAnyware() -// core = anyware.pipeline -// }) +beforeEach(() => { + builder = createPipeline() +}) -// export const runWithOptions = (options: Options = {}) => async (...interceptors: InterceptorInput[]) => { -// const result = await anyware.run({ -// initialInput, -// // @ts-expect-error fixme -// interceptors, -// options, -// }) -// return result -// } +export const runWithOptions = (options: Options = {}) => async (...interceptors: InterceptorInput[]) => { + return await Pipeline.run(builder, { + initialInput, + // @ts-expect-error fixme + interceptors, + options, + }) +} -// export const run = async (...extensions: InterceptorInput[]) => runWithOptions({})(...extensions) +export const run = async (...extensions: InterceptorInput[]) => runWithOptions({})(...extensions) -// export const oops = new Error(`oops`) +export const oops = new Error(`oops`) diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index a7ffd4225..5ca056421 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -6,11 +6,7 @@ import type { Private } from '../../private.js' import type { HookName } from '../hook/definition.js' import type { HookResultErrorExtension } from '../hook/private.js' import type { SomePublicStepEnvelope } from '../hook/public.js' -import { - createRetryingInterceptor, - type InferInterceptorConstructor, - type InterceptorInput, -} from '../Interceptor/Interceptor.js' +import { createRetryingInterceptor, type Interceptor, type InterceptorInput } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' import { getEntrypoint } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' @@ -18,8 +14,8 @@ import { runPipeline } from './runPipeline.js' export type Runner<$Pipeline extends Pipeline = Pipeline> = ( { initialInput, interceptors, options }: { initialInput: GetInitialPipelineInput<$Pipeline> - interceptors: InferInterceptorConstructor<$Pipeline>[] - retryingInterceptor?: InferInterceptorConstructor<$Pipeline, { retrying: true }> + interceptors: Interceptor.InferConstructor<$Pipeline>[] + retryingInterceptor?: Interceptor.InferConstructor<$Pipeline, { retrying: true }> options?: Options }, ) => Promise['result'] | Errors.ContextualError> diff --git a/src/lib/config-manager/ConfigManager.ts b/src/lib/config-manager/ConfigManager.ts index ccf4707cb..9cd0c74a7 100644 --- a/src/lib/config-manager/ConfigManager.ts +++ b/src/lib/config-manager/ConfigManager.ts @@ -1,6 +1,14 @@ -import type { PartialDeep, Simplify } from 'type-fest' +import type { IsUnknown, PartialDeep, Simplify } from 'type-fest' import { isDate } from 'util/types' -import { type ExcludeUndefined, type GuardedType, isAnyFunction, isNonNullObject, type OrDefault } from '../prelude.js' +import { type ExcludeUndefined, type GuardedType, isAnyFunction, isNonNullObject } from '../prelude.js' + +// dprint-ignore +export type OrDefault<$Value, $Default> = + // When no value has been passed in because the property is optional, + // then the inferred type is unknown. + IsUnknown<$Value> extends true ? $Default : + $Value extends undefined ? $Default : + $Value // dprint-ignore export type MergeDefaults<$Defaults extends object, $Input extends undefined | object, $CustomScalars> = diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index d7fc2d101..04ed003e1 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -1,4 +1,5 @@ import type { HasRequiredKeys, IsAny, IsEmptyObject, IsNever, IsUnknown, Simplify } from 'type-fest' +import type { ConfigManager } from './config-manager/__.js' /* eslint-disable */ export type RemoveIndex = { @@ -298,14 +299,6 @@ export const debugSub = (...args: any[]) => (...subArgs: any[]) => { debug(...args, ...subArgs) } -// dprint-ignore -export type OrDefault<$Value, $Default> = - // When no value has been passed in because the property is optional, - // then the inferred type is unknown. - IsUnknown<$Value> extends true ? $Default : - $Value extends undefined ? $Default : - $Value - export namespace Tuple { // dprint-ignore export type GetAtNextIndex<$Items extends readonly any[], $Index extends NumberLiteral> = @@ -313,7 +306,7 @@ export namespace Tuple { // dprint-ignore export type GetNextIndexOr<$Items extends readonly any[], $Index extends number, $Or> = - OrDefault, $Or> + ConfigManager.OrDefault, $Or> // dprint-ignore export type DropUntilIndex<$Items extends readonly any[], $Index extends NumberLiteral> = diff --git a/tests/_/SpyExtension.ts b/tests/_/SpyExtension.ts index 691390fd3..abdb26c01 100644 --- a/tests/_/SpyExtension.ts +++ b/tests/_/SpyExtension.ts @@ -1,17 +1,17 @@ import { beforeEach } from 'vitest' import { createExtension } from '../../src/entrypoints/main.js' import type { Config } from '../../src/entrypoints/utilities-for-generated.js' -import type { HookDefEncode, HookDefExchange, HookDefPack } from '../../src/requestPipeline/types.js' +import type { RequestPipeline } from '../../src/requestPipeline/__.js' interface SpyData { encode: { - input: HookDefEncode['input'] | null + input: RequestPipeline.Hooks.HookDefEncode['input'] | null } pack: { - input: HookDefPack['input'] | null + input: RequestPipeline.Hooks.HookDefPack['input'] | null } exchange: { - input: HookDefExchange['input'] | null + input: RequestPipeline.Hooks.HookDefExchange['input'] | null } } From 0ebccc548a82c774a22f7e272e26553ee092416e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 6 Nov 2024 07:26:46 -0500 Subject: [PATCH 11/36] work --- src/ClientPreset/ClientPreset.ts | 4 +- src/documentBuilder/InferResult/Alias.ts | 4 +- src/lib/anyware/Pipeline/builder.test-d.ts | 2 +- src/lib/anyware/Pipeline/builder.ts | 32 +++-- src/lib/anyware/Pipeline/run.test-d.ts | 22 +-- src/lib/anyware/__.test-helpers.ts | 11 +- src/lib/anyware/__.test.ts | 160 +++++++++++---------- src/lib/builder/Definition.ts | 12 +- src/lib/prelude.test-d.ts | 3 + src/lib/prelude.ts | 23 ++- 10 files changed, 160 insertions(+), 113 deletions(-) diff --git a/src/ClientPreset/ClientPreset.ts b/src/ClientPreset/ClientPreset.ts index f8be27b87..bf7710e23 100644 --- a/src/ClientPreset/ClientPreset.ts +++ b/src/ClientPreset/ClientPreset.ts @@ -14,7 +14,7 @@ import type { } from '../extension/extension.js' import type { Builder } from '../lib/builder/__.js' import type { ConfigManager } from '../lib/config-manager/__.js' -import { type intersectArrayOfObjects, type ToParametersExact } from '../lib/prelude.js' +import { type ToParametersExact, type Tuple } from '../lib/prelude.js' import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js' import { Schema } from '../types/Schema/__.js' import type { SchemaDrivenDataMap } from '../types/SchemaDrivenDataMap/__.js' @@ -101,7 +101,7 @@ type ConstructorParameters< $Extensions extends [...ExtensionConstructor[]], > = & InputBase> - & intersectArrayOfObjects> + & Tuple.IntersectItems> // dprint-ignore type GetParametersContributedByExtensions = { diff --git a/src/documentBuilder/InferResult/Alias.ts b/src/documentBuilder/InferResult/Alias.ts index c64c493b8..ad214e2e8 100644 --- a/src/documentBuilder/InferResult/Alias.ts +++ b/src/documentBuilder/InferResult/Alias.ts @@ -1,4 +1,4 @@ -import type { intersectArrayOfObjects, ValuesOrEmptyObject } from '../../lib/prelude.js' +import type { Tuple, ValuesOrEmptyObject } from '../../lib/prelude.js' import type { Schema } from '../../types/Schema/__.js' import type { Select } from '../Select/__.js' import type { OutputField } from './OutputField.js' @@ -42,7 +42,7 @@ type InferSelectAliasMultiple< $FieldName extends string, $Schema extends Schema, $Node extends Schema.OutputObject, -> = intersectArrayOfObjects< +> = Tuple.IntersectItems< { [_ in keyof $SelectAliasMultiple]: InferSelectAliasOne<$SelectAliasMultiple[_], $FieldName, $Schema, $Node> } diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/Pipeline/builder.test-d.ts index f08b496d5..d6a06f227 100644 --- a/src/lib/anyware/Pipeline/builder.test-d.ts +++ b/src/lib/anyware/Pipeline/builder.test-d.ts @@ -6,7 +6,7 @@ import { Pipeline } from './__.js' const p0 = Pipeline.create() test(`initial context`, () => { - expectTypeOf(p0.context).toEqualTypeOf<{ input: initialInput; output: object; steps: [] }>() + expectTypeOf(p0.context).toEqualTypeOf<{ input: initialInput; output: object; steps: []; stepsIndex: {} }>() }) test(`first step definition`, () => { diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index cacebe10d..93f320c54 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,5 +1,5 @@ import type { ConfigManager } from '../../config-manager/__.js' -import { type GetLastValue, type intersectArrayOfObjects } from '../../prelude.js' +import { type GetLastValue, type Tuple } from '../../prelude.js' import type { Pipeline } from './__.js' export { type HookDefinitionMap } from '../hook/definition.js' @@ -16,14 +16,16 @@ export interface Step< export interface Context { input: object steps: Step[] + stepsIndex: Record } export interface ContextEmpty extends Context { input: object output: object steps: [] + stepsIndex: {} + passthroughErrorWith: undefined } - export namespace Step { export type GetAwaitedResult<$Step extends Step> = Awaited> export type GetResult<$Step extends Step> = ReturnType<$Step['run']> @@ -52,7 +54,7 @@ export type GetNextStepParameterPrevious<$Pipeline extends Pipeline> = ? GetNextStepPrevious_<$Pipeline['steps']> : undefined -type GetNextStepPrevious_<$Steps extends Step[]> = intersectArrayOfObjects< +type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< { [$Index in keyof $Steps]: { [$StepName in $Steps[$Index]['name']]: { @@ -110,13 +112,16 @@ export interface Builder<$Context extends Context = Context> { ConfigManager.SetOneKey< $Context, 'steps', - [...$Context['steps'], { - name: $Name - run: $Run - input: $Params['input'] - output: ReturnType<$Run> - slots: $Slots - }] + [ + ...$Context['steps'], + { + name: $Name + run: $Run + input: $Params['input'] + output: ReturnType<$Run> + slots: $Slots + }, + ] > > } @@ -126,6 +131,11 @@ export type Infer<$Builder extends Builder> = $Builder['context'] /** * TODO */ -export const create = <$Input extends object>(): Builder<{ input: $Input; steps: []; output: object }> => { +export const create = <$Input extends object>(): Builder<{ + input: $Input + steps: [] + stepsIndex: {} + output: object +}> => { return undefined as any } diff --git a/src/lib/anyware/Pipeline/run.test-d.ts b/src/lib/anyware/Pipeline/run.test-d.ts index 521e229b1..af677736b 100644 --- a/src/lib/anyware/Pipeline/run.test-d.ts +++ b/src/lib/anyware/Pipeline/run.test-d.ts @@ -1,20 +1,22 @@ import { expectTypeOf, test } from 'vitest' +import type { ContextualAggregateError } from '../../errors/ContextualAggregateError.js' +import type { initialInput } from '../__.test-helpers.js' import { Pipeline } from './__.js' -test(`returns input if no steps`, () => { - const p = Pipeline.create<{ x: 1 }>() - const r = Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf<{ x: 1 }>() +test(`returns input if no steps`, async () => { + const p = Pipeline.create() + const r = await Pipeline.run(p) + expectTypeOf(r).toEqualTypeOf() }) -test(`returns last step output if steps`, () => { - const p = Pipeline.create<{ x: 1 }>().step({ name: `a`, run: () => 2 as const }) - const r = Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf<2>() +test(`returns last step output if steps`, async () => { + const p = Pipeline.create().step({ name: `a`, run: () => 2 as const }) + const r = await Pipeline.run(p) + expectTypeOf(r).toEqualTypeOf<2 | ContextualAggregateError>() }) test(`can return a promise`, async () => { - const p = Pipeline.create<{ x: 1 }>().step({ name: `a`, run: () => Promise.resolve(2 as const) }) + const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(2 as const) }) const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf<2>() + expectTypeOf(r).toEqualTypeOf<2 | ContextualAggregateError>() }) diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index e9bf3954a..1cfeaa3a0 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -1,6 +1,7 @@ +import { keyBy } from 'es-toolkit' import { beforeEach, vi } from 'vitest' +import type { Tuple } from '../prelude.js' import { Pipeline } from './_.js' -import type { Anyware } from './__.js' import type { InterceptorInput } from './Interceptor/Interceptor.js' import type { Options } from './run/runner.js' @@ -60,10 +61,16 @@ export const createPipeline = () => { }) } +type TestBuilder = ReturnType + +// @ts-expect-error +export let builder: TestBuilder = null + // @ts-expect-error -export let builder: Anyware.Builder<$Core> = null +export let stepsIndex: Tuple.ToIndexByObjectKey = null beforeEach(() => { + stepsIndex = keyBy(builder.context.steps, _ => _.name) as any builder = createPipeline() }) diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts index 242563180..c5779dff6 100644 --- a/src/lib/anyware/__.test.ts +++ b/src/lib/anyware/__.test.ts @@ -3,8 +3,8 @@ import { describe, expect, test, vi } from 'vitest' import { Errors } from '../errors/__.js' import type { ContextualError } from '../errors/ContextualError.js' -import { Anyware } from './__.js' -import { core, createHook, initialInput, oops, run, runWithOptions } from './__.test-helpers.js' +import { Pipeline } from './_.js' +import { initialInput, oops, run, runWithOptions, stepsIndex } from './__.test-helpers.js' import { createRetryingInterceptor } from './Interceptor/Interceptor.js' describe(`no extensions`, () => { @@ -23,9 +23,9 @@ describe(`one extension`, () => { return 0 }), ).toEqual(0) - expect(core.hooks.a.run.mock.calls[0]).toMatchObject([{ input: { value: `initial` } }]) - expect(core.hooks.a.run).toHaveBeenCalled() - expect(core.hooks.b.run).toHaveBeenCalled() + expect(stepsIndex.a.run.mock.calls[0]).toMatchObject([{ input: { value: `initial` } }]) + expect(stepsIndex.a.run).toHaveBeenCalled() + expect(stepsIndex.b.run).toHaveBeenCalled() }) test('can call hook with no input, making the original input be used', () => { expect( @@ -45,8 +45,8 @@ describe(`one extension`, () => { return a.input }), ).toEqual({ value: `initial` }) - expect(core.hooks.a.run).not.toHaveBeenCalled() - expect(core.hooks.b.run).not.toHaveBeenCalled() + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() }) test(`at start, return own result`, async () => { expect( @@ -55,8 +55,8 @@ describe(`one extension`, () => { return 0 }), ).toEqual(0) - expect(core.hooks.a.run).not.toHaveBeenCalled() - expect(core.hooks.b.run).not.toHaveBeenCalled() + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() }) test(`after first hook, return own result`, async () => { expect( @@ -65,7 +65,7 @@ describe(`one extension`, () => { return b.input.value + `+x` }), ).toEqual(`initial+a+x`) - expect(core.hooks.b.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() }) }) describe(`can partially apply`, () => { @@ -101,8 +101,8 @@ describe(`two extensions`, () => { const ex2 = vi.fn().mockImplementation(() => 2) expect(await run(ex1, ex2)).toEqual(1) expect(ex2).not.toHaveBeenCalled() - expect(core.hooks.a.run).not.toHaveBeenCalled() - expect(core.hooks.b.run).not.toHaveBeenCalled() + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() }) test(`each can adjust first hook then passthrough`, async () => { @@ -144,8 +144,8 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual(2) expect(ex1AfterA).toBe(false) - expect(core.hooks.a.run).not.toHaveBeenCalled() - expect(core.hooks.b.run).not.toHaveBeenCalled() + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() }) test(`second can short-circuit after hook a`, async () => { let ex1AfterB = false @@ -160,8 +160,8 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual(2) expect(ex1AfterB).toBe(false) - expect(core.hooks.a.run).toHaveBeenCalledOnce() - expect(core.hooks.b.run).not.toHaveBeenCalled() + expect(stepsIndex.a.run).toHaveBeenCalledOnce() + expect(stepsIndex.b.run).not.toHaveBeenCalled() }) }) @@ -208,7 +208,7 @@ describe(`errors`, () => { }) test(`if implementation fails, without extensions, result is the error`, async () => { - core.hooks.a.run.mockReset().mockRejectedValueOnce(oops) + stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops) const result = await run() as ContextualError expect({ result, @@ -244,59 +244,69 @@ describe(`errors`, () => { } `) }) - describe('certain errors can be configured to be re-thrown without wrapping error', () => { - class SpecialError1 extends Error {} - class SpecialError2 extends Error {} - const a = createHook({ - slots: {}, - run: ({ input }: { slots: object; input: { throws: Error } }) => { - if (input.throws) throw input.throws - }, - }) + // describe.skip('certain errors can be configured to be re-thrown without wrapping error', () => { + // class SpecialError1 extends Error {} + // class SpecialError2 extends Error {} + // // const a = createHook({ + // // slots: {}, + // // run: ({ input }: { slots: object; input: { throws: Error } }) => { + // // if (input.throws) throw input.throws + // // }, + // // }) - test('via passthroughErrorInstanceOf (one)', async () => { - const anyware = Anyware.create<['a'], Anyware.HookDefinitionMap<['a']>>({ - hookNamesOrderedBySequence: [`a`], - hooks: { a }, - passthroughErrorInstanceOf: [SpecialError1], - }) - // dprint-ignore - expect(anyware.run({ initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // dprint-ignore - expect(anyware.run({ initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) - }) - test('via passthroughErrorInstanceOf (multiple)', async () => { - const anyware = Anyware.create<['a'], Anyware.HookDefinitionMap<['a']>>({ - hookNamesOrderedBySequence: [`a`], - hooks: { a }, - passthroughErrorInstanceOf: [SpecialError1, SpecialError2], - }) - // dprint-ignore - expect(anyware.run({ initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // dprint-ignore - expect(anyware.run({ initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) - }) - test('via passthroughWith', async () => { - const anyware = Anyware.create<['a'], Anyware.HookDefinitionMap<['a']>>({ - hookNamesOrderedBySequence: [`a`], - hooks: { a }, - // todo type-safe hook name according to values passed to constructor - // todo type-tests on signal { hookName, source, error } - passthroughErrorWith: (signal) => { - return signal.error instanceof SpecialError1 - }, - }) - // dprint-ignore - expect(anyware.run({ initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // dprint-ignore - expect(anyware.run({ initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) - }) - }) + // test('via passthroughErrorInstanceOf (one)', async () => { + // const builder = Pipeline.create<{ throws: Error }>({ + // passthroughErrorInstanceOf: [SpecialError1], + // }).step({ + // name: 'a', + // run: ({ input }) => { + // if (input.throws) throw input.throws + // }, + // }) + + // // dprint-ignore + // expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // // dprint-ignore + // expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + // }) + // test('via passthroughErrorInstanceOf (multiple)', async () => { + // const builder = Pipeline.create<{ throws: Error }>({ + // passthroughErrorInstanceOf: [SpecialError1, SpecialError2], + // }).step({ + // name: 'a', + // run: ({ input }) => { + // if (input.throws) throw input.throws + // }, + // }) + // // dprint-ignore + // expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // // dprint-ignore + // expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) + // }) + // test('via passthroughWith', async () => { + // const builder = Pipeline.create<{ throws: Error }>({ + // // todo type-safe hook name according to values passed to constructor + // // todo type-tests on signal { hookName, source, error } + // passthroughErrorWith: (signal) => { + // return signal.error instanceof SpecialError1 + // }, + // }).step({ + // name: 'a', + // run: ({ input }) => { + // if (input.throws) throw input.throws + // }, + // }) + // // dprint-ignore + // expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // // dprint-ignore + // expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + // }) + // }) }) describe('retrying extension', () => { test('if hook fails, extension can retry, then short-circuit', async () => { - core.hooks.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) + stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) const result = await run(createRetryingInterceptor(async function foo({ a }) { const result1 = await a() expect(result1).toEqual(oops) @@ -362,15 +372,15 @@ describe('retrying extension', () => { describe('slots', () => { test('have defaults that are called by default', async () => { await run() - expect(core.hooks.a.slots.append.mock.calls[0]).toMatchObject(['a']) - expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) + expect(stepsIndex.a.slots.append.mock.calls[0]).toMatchObject(['a']) + expect(stepsIndex.b.slots.append.mock.calls[0]).toMatchObject(['b']) }) test('extension can provide own function to slot on just one of a set of hooks', async () => { const result = await run(async ({ a }) => { return a({ using: { append: () => 'x' } }) }) - expect(core.hooks.a.slots.append).not.toBeCalled() - expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) + expect(stepsIndex.a.slots.append).not.toBeCalled() + expect(stepsIndex.b.slots.append.mock.calls[0]).toMatchObject(['b']) expect(result).toEqual({ value: 'initial+x+b' }) }) test('extension can provide own functions to slots on multiple of a set of hooks', async () => { @@ -385,8 +395,8 @@ describe('slots', () => { const { b } = await a({ using: { append: () => 'x' } }) return b({ using: { append: () => 'y' } }) }) - expect(core.hooks.a.slots.append).not.toBeCalled() - expect(core.hooks.b.slots.append).not.toBeCalled() + expect(stepsIndex.a.slots.append).not.toBeCalled() + expect(stepsIndex.b.slots.append).not.toBeCalled() expect(result).toEqual({ value: 'initial+x+y' }) }) }) @@ -396,8 +406,8 @@ describe('private hook parameter - previous', () => { await run(async ({ a }) => { return a() }) - expect(core.hooks.a.run.mock.calls[0]?.[0].previous).toEqual({}) - expect(core.hooks.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput } }) + expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) + expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput } }) }) test('contains the final input actually passed to the hook', async () => { @@ -405,7 +415,7 @@ describe('private hook parameter - previous', () => { await run(async ({ a }) => { return a({ input: customInput }) }) - expect(core.hooks.a.run.mock.calls[0]?.[0].previous).toEqual({}) - expect(core.hooks.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: customInput } }) + expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) + expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: customInput } }) }) }) diff --git a/src/lib/builder/Definition.ts b/src/lib/builder/Definition.ts index f39d4a368..df74707e9 100644 --- a/src/lib/builder/Definition.ts +++ b/src/lib/builder/Definition.ts @@ -1,5 +1,5 @@ import type { Simplify } from 'type-fest' -import type { AssertExtends, intersectArrayOfObjects } from '../prelude.js' +import type { AssertExtends, Tuple } from '../prelude.js' import type { Private } from '../private.js' import type { TypeFunction } from '../type-function/__.js' import type { Context, Extension } from './Extension.js' @@ -55,11 +55,11 @@ export type MaterializeGeneric<$Chain_ extends Definition_> = Private.Add< { chain: $Chain_, - context: intersectArrayOfObjects< + context: Tuple.IntersectItems< MaterializeExtensionsGenericContext<$Chain_['extensions']> > }, - intersectArrayOfObjects< + Tuple.IntersectItems< MaterializeExtensionsGeneric<$Chain_, $Chain_['extensions']> > > @@ -83,11 +83,11 @@ export type MaterializeSpecific<$Chain_ extends Definition_> = Private.Add< { chain: $Chain_, - context: intersectArrayOfObjects< + context: Tuple.IntersectItems< MaterializeExtensionsInitialContext<$Chain_['extensions']> > }, - intersectArrayOfObjects< + Tuple.IntersectItems< MaterializeExtensionsInitial<$Chain_, $Chain_['extensions']> > > @@ -113,7 +113,7 @@ export type MaterializeWithNewContext<$Chain_ extends Definition_, $Context exte chain: $Chain_, context: $Context }, - intersectArrayOfObjects< + Tuple.IntersectItems< MaterializeExtensionsWithNewState< $Chain_, $Context, diff --git a/src/lib/prelude.test-d.ts b/src/lib/prelude.test-d.ts index 63ab858f9..59acee09f 100644 --- a/src/lib/prelude.test-d.ts +++ b/src/lib/prelude.test-d.ts @@ -28,4 +28,7 @@ assertEqual, undefined>() assertEqual, 2>() assertEqual, false>() + +assertEqual, { a: { name: 'a' }, b: { name: 'b' } }>() +assertEqual, {}>() } diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 04ed003e1..746e38bcb 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -300,6 +300,25 @@ export const debugSub = (...args: any[]) => (...subArgs: any[]) => { } export namespace Tuple { + // dprint-ignore + export type IntersectItems<$Items extends readonly any[]> = + $Items extends [infer $First, ...infer $Rest extends any[]] + ? $First & IntersectItems<$Rest> + : {} + + // dprint-ignore + export type ToIndexByObjectKey<$Items extends readonly object[], $Key extends keyof $Items[number]> = + Simplify< + IntersectItems<{ + [$Index in keyof $Items]: + $Key extends keyof $Items[$Index] + ? { + [_ in $Items[$Index][$Key] & string]: $Items[$Index] + } + : never + }> + > + // dprint-ignore export type GetAtNextIndex<$Items extends readonly any[], $Index extends NumberLiteral> = $Items[PlusOne<$Index>] @@ -482,10 +501,6 @@ export const shallowMergeDefaults = <$Defaults extends object, $Input extends ob return merged as any } -export type intersectArrayOfObjects = T extends [infer $First, ...infer $Rest extends any[]] - ? $First & intersectArrayOfObjects<$Rest> - : {} - export const identityProxy = new Proxy({}, { get: () => (value: unknown) => value, }) From 2b096f09ef07a696dc1fe203752dcdb309a6d7bd Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 6 Nov 2024 07:41:40 -0500 Subject: [PATCH 12/36] remove hook def --- src/lib/anyware/Pipeline/builder.ts | 29 ++++++++++++++++++++++++---- src/lib/anyware/Step/Step.ts | 1 + src/lib/anyware/Step/__.ts | 1 + src/lib/anyware/hook/definition.ts | 13 ------------- src/lib/anyware/hook/private.ts | 28 +++++++++++++-------------- src/lib/anyware/hook/public.ts | 1 - src/lib/anyware/run/getEntrypoint.ts | 4 ++-- src/lib/anyware/run/runner.ts | 4 ++-- 8 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 src/lib/anyware/Step/Step.ts create mode 100644 src/lib/anyware/Step/__.ts delete mode 100644 src/lib/anyware/hook/definition.ts diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 93f320c54..5c125e2a7 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,8 +1,10 @@ import type { ConfigManager } from '../../config-manager/__.js' import { type GetLastValue, type Tuple } from '../../prelude.js' +import type { HookResultError } from '../hook/private.js' import type { Pipeline } from './__.js' -export { type HookDefinitionMap } from '../hook/definition.js' +// export { type HookDefinitionMap } from '../hook/definition.js' + export interface Step< $Name extends string = string, > { @@ -13,7 +15,7 @@ export interface Step< run: (params: any) => any } -export interface Context { +export interface Context extends Options { input: object steps: Step[] stepsIndex: Record @@ -24,8 +26,8 @@ export interface ContextEmpty extends Context { output: object steps: [] stepsIndex: {} - passthroughErrorWith: undefined } + export namespace Step { export type GetAwaitedResult<$Step extends Step> = Awaited> export type GetResult<$Step extends Step> = ReturnType<$Step['run']> @@ -128,14 +130,33 @@ export interface Builder<$Context extends Context = Context> { export type Infer<$Builder extends Builder> = $Builder['context'] +interface Options { + /** + * If a hook results in a thrown error but is an instance of one of these classes then return it as-is + * rather than wrapping it in a ContextualError. + * + * This can be useful when there are known kinds of errors such as Abort Errors from AbortController + * which are actually a signaling mechanism. + */ + passthroughErrorInstanceOf?: Function[] + /** + * If a hook results in a thrown error but returns true from this function then return the error as-is + * rather than wrapping it in a ContextualError. + * + * This can be useful when there are known kinds of errors such as Abort Errors from AbortController + * which are actually a signaling mechanism. + */ + passthroughErrorWith?: (signal: HookResultError) => boolean +} /** * TODO */ -export const create = <$Input extends object>(): Builder<{ +export const create = <$Input extends object>(options?: Options): Builder<{ input: $Input steps: [] stepsIndex: {} output: object }> => { + options return undefined as any } diff --git a/src/lib/anyware/Step/Step.ts b/src/lib/anyware/Step/Step.ts new file mode 100644 index 000000000..47f1e5707 --- /dev/null +++ b/src/lib/anyware/Step/Step.ts @@ -0,0 +1 @@ +export type Name = string diff --git a/src/lib/anyware/Step/__.ts b/src/lib/anyware/Step/__.ts new file mode 100644 index 000000000..c261638b9 --- /dev/null +++ b/src/lib/anyware/Step/__.ts @@ -0,0 +1 @@ +export * as Step from './Step.js' diff --git a/src/lib/anyware/hook/definition.ts b/src/lib/anyware/hook/definition.ts deleted file mode 100644 index 57f6c57d8..000000000 --- a/src/lib/anyware/hook/definition.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type HookSequence = readonly [string, ...string[]] - -export type HookDefinitionMap<$HookSequence extends HookSequence> = Record< - $HookSequence[number], - HookDefinition -> - -export type HookDefinition = { - input: any /* object <- type error but more accurate */ - slots?: any /* object <- type error but more accurate */ -} - -export type HookName = string diff --git a/src/lib/anyware/hook/private.ts b/src/lib/anyware/hook/private.ts index a3fa4e459..7d65022de 100644 --- a/src/lib/anyware/hook/private.ts +++ b/src/lib/anyware/hook/private.ts @@ -1,21 +1,21 @@ import type { Errors } from '../../errors/__.js' import type { Deferred, MaybePromise, SomeFunction, TakeValuesBefore } from '../../prelude.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -import type { HookDefinitionMap, HookSequence } from './definition.js' +// import type { HookDefinitionMap, HookSequence } from './definition.js' -export type InferPrivateHookInput< - $HookSequence extends HookSequence, - $HookMap extends HookDefinitionMap<$HookSequence>, - $HookName extends string, -> = HookPrivateInput< - $HookMap[$HookName]['input'], - $HookMap[$HookName]['slots'], - { - [$PreviousHookName in TakeValuesBefore<$HookName, $HookSequence>[number]]: { - input: $HookMap[$PreviousHookName]['input'] - } - } -> +// export type InferPrivateHookInput< +// $HookSequence extends HookSequence, +// $HookMap extends HookDefinitionMap<$HookSequence>, +// $HookName extends string, +// > = HookPrivateInput< +// $HookMap[$HookName]['input'], +// $HookMap[$HookName]['slots'], +// { +// [$PreviousHookName in TakeValuesBefore<$HookName, $HookSequence>[number]]: { +// input: $HookMap[$PreviousHookName]['input'] +// } +// } +// > export type PrivateHook<$Slots extends Slots, $Input extends HookPrivateInput, $Return> = { slots: $Slots diff --git a/src/lib/anyware/hook/public.ts b/src/lib/anyware/hook/public.ts index e4a7460ca..8dd46547a 100644 --- a/src/lib/anyware/hook/public.ts +++ b/src/lib/anyware/hook/public.ts @@ -1,7 +1,6 @@ import type { FindValueAfter, IsLastValue } from '../../prelude.js' import type { InterceptorOptions } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' -import type { HookDefinition, HookDefinitionMap, HookSequence } from './definition.js' export type InferPublicHooks< $Pipeline extends Pipeline, diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index cd67d48e4..99bb15782 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,7 +1,7 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' -import type { HookName } from '../hook/definition.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' +import type { Step } from '../Step/__.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< 'ErrorGraffleInterceptorEntryHook', @@ -26,7 +26,7 @@ export type InterceptorEntryHookIssue = typeof InterceptorEntryHookIssue[keyof t export const getEntrypoint = ( hookNames: readonly string[], interceptor: NonRetryingInterceptorInput, -): ErrorAnywareInterceptorEntrypoint | HookName => { +): ErrorAnywareInterceptorEntrypoint | Step.Name => { const x = analyzeFunction(interceptor) if (x.parameters.length > 1) { return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.multipleParameters }) diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 5ca056421..7cbfd898c 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -3,11 +3,11 @@ import { Errors } from '../../errors/__.js' import { createDeferred } from '../../prelude.js' import { casesExhausted } from '../../prelude.js' import type { Private } from '../../private.js' -import type { HookName } from '../hook/definition.js' import type { HookResultErrorExtension } from '../hook/private.js' import type { SomePublicStepEnvelope } from '../hook/public.js' import { createRetryingInterceptor, type Interceptor, type InterceptorInput } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' +import type { Step } from '../Step/__.js' import { getEntrypoint } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' @@ -109,7 +109,7 @@ const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: } } - const hooksBeforeEntrypoint: HookName[] = [] + const hooksBeforeEntrypoint: Step.Name[] = [] for (const hookName of pipeline.hookNamesOrderedBySequence) { if (hookName === entrypoint) break hooksBeforeEntrypoint.push(hookName) From e7fa215599ab8739826e5a9c139218e18dd82596 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 6 Nov 2024 09:12:40 -0500 Subject: [PATCH 13/36] wip --- .../anyware/Interceptor/Interceptor.test-d.ts | 6 +- src/lib/anyware/Interceptor/Interceptor.ts | 8 +- src/lib/anyware/Pipeline/builder.ts | 43 ++++---- src/lib/anyware/Step/Step.ts | 6 + src/lib/anyware/Step/__.ts | 10 ++ src/lib/anyware/hook/private.ts | 2 +- src/lib/anyware/hook/public.ts | 104 +++++++++--------- src/lib/anyware/run/OptimizedPipeline.ts | 12 ++ src/lib/anyware/run/getEntrypoint.ts | 24 ++-- src/lib/anyware/run/runPipeline.ts | 69 ++++++------ .../anyware/run/{runHook.ts => runStep.ts} | 104 +++++++++--------- src/lib/anyware/run/runner.ts | 90 ++++++--------- src/lib/prelude.ts | 69 ++++++------ 13 files changed, 282 insertions(+), 265 deletions(-) create mode 100644 src/lib/anyware/run/OptimizedPipeline.ts rename src/lib/anyware/run/{runHook.ts => runStep.ts} (78%) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 7f2eda34f..4a73c44b1 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -3,7 +3,7 @@ import { _, type ExcludeUndefined } from '../../prelude.js' import { type Interceptor, Pipeline } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' -import type { SomePublicStepEnvelope } from '../hook/public.js' +import type { SomeStepTriggerEnvelope } from '../hook/public.js' import type { Builder } from '../Pipeline/builder.js' const p0 = Pipeline.create() @@ -77,12 +77,12 @@ describe(`interceptor constructor`, () => { // test(`can return pipeline output or a step envelope`, () => { const p = p0.step({ name: `a`, run: () => results.a }) - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index 1693a0f4b..f9f5483e5 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,6 +1,6 @@ import type { Simplify } from 'type-fest' import type { Deferred, Func, MaybePromise } from '../../prelude.js' -import type { SomePublicStepEnvelope } from '../hook/public.js' +import type { SomeStepTriggerEnvelope } from '../hook/public.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Pipeline/builder.js' @@ -25,7 +25,7 @@ export namespace Interceptor { steps: Simplify>, ): Promise< | Pipeline.GetAwaitedResult<$Pipeline> - | SomePublicStepEnvelope + | SomeStepTriggerEnvelope > } @@ -79,7 +79,7 @@ export type NonRetryingInterceptor = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export type RetryingInterceptor = { @@ -87,7 +87,7 @@ export type RetryingInterceptor = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export const createRetryingInterceptor = (extension: NonRetryingInterceptorInput): RetryingInterceptorInput => { diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 5c125e2a7..fc7abaa0b 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,36 +1,21 @@ import type { ConfigManager } from '../../config-manager/__.js' import { type GetLastValue, type Tuple } from '../../prelude.js' import type { HookResultError } from '../hook/private.js' +import type { Step } from '../Step/__.js' import type { Pipeline } from './__.js' // export { type HookDefinitionMap } from '../hook/definition.js' -export interface Step< - $Name extends string = string, -> { - name: $Name - slots: object | undefined - input: any - output: any - run: (params: any) => any -} - -export interface Context extends Options { +export interface Context { input: object steps: Step[] - stepsIndex: Record + config: Config } export interface ContextEmpty extends Context { input: object - output: object steps: [] - stepsIndex: {} -} - -export namespace Step { - export type GetAwaitedResult<$Step extends Step> = Awaited> - export type GetResult<$Step extends Step> = ReturnType<$Step['run']> + config: Config } /** @@ -131,6 +116,10 @@ export interface Builder<$Context extends Context = Context> { export type Infer<$Builder extends Builder> = $Builder['context'] interface Options { + /** + * @defaultValue `required` + */ + entrypointSelectionMode?: 'optional' | 'required' | 'off' /** * If a hook results in a thrown error but is an instance of one of these classes then return it as-is * rather than wrapping it in a ContextualError. @@ -146,8 +135,11 @@ interface Options { * This can be useful when there are known kinds of errors such as Abort Errors from AbortController * which are actually a signaling mechanism. */ - passthroughErrorWith?: (signal: HookResultError) => boolean + passthroughErrorWith?: null | ((signal: HookResultError) => boolean) } + +type Config = Required + /** * TODO */ @@ -156,7 +148,16 @@ export const create = <$Input extends object>(options?: Options): Builder<{ steps: [] stepsIndex: {} output: object + config: Config }> => { - options + const config = resolveOptions(options) return undefined as any } + +const resolveOptions = (options?: Options): Config => { + return { + passthroughErrorInstanceOf: options?.passthroughErrorInstanceOf ?? [], + passthroughErrorWith: options?.passthroughErrorWith ?? null, + entrypointSelectionMode: options?.entrypointSelectionMode ?? `required`, + } +} diff --git a/src/lib/anyware/Step/Step.ts b/src/lib/anyware/Step/Step.ts index 47f1e5707..af361f78e 100644 --- a/src/lib/anyware/Step/Step.ts +++ b/src/lib/anyware/Step/Step.ts @@ -1 +1,7 @@ +import type { Step } from './__.js' + export type Name = string + +export type GetAwaitedResult<$Step extends Step> = Awaited> + +export type GetResult<$Step extends Step> = ReturnType<$Step['run']> diff --git a/src/lib/anyware/Step/__.ts b/src/lib/anyware/Step/__.ts index c261638b9..841ede02d 100644 --- a/src/lib/anyware/Step/__.ts +++ b/src/lib/anyware/Step/__.ts @@ -1 +1,11 @@ export * as Step from './Step.js' + +export interface Step< + $Name extends string = string, +> { + name: $Name + slots: object | undefined + input: any + output: any + run: (params: any) => any +} diff --git a/src/lib/anyware/hook/private.ts b/src/lib/anyware/hook/private.ts index 7d65022de..ea4fa3089 100644 --- a/src/lib/anyware/hook/private.ts +++ b/src/lib/anyware/hook/private.ts @@ -1,5 +1,5 @@ import type { Errors } from '../../errors/__.js' -import type { Deferred, MaybePromise, SomeFunction, TakeValuesBefore } from '../../prelude.js' +import type { Deferred, MaybePromise, SomeFunction } from '../../prelude.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' // import type { HookDefinitionMap, HookSequence } from './definition.js' diff --git a/src/lib/anyware/hook/public.ts b/src/lib/anyware/hook/public.ts index 8dd46547a..ab659562c 100644 --- a/src/lib/anyware/hook/public.ts +++ b/src/lib/anyware/hook/public.ts @@ -1,28 +1,28 @@ -import type { FindValueAfter, IsLastValue } from '../../prelude.js' import type { InterceptorOptions } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' +import type { Step } from '../Step/__.js' -export type InferPublicHooks< - $Pipeline extends Pipeline, - // $HookSequence extends HookSequence, - // $HookMap extends Record<$HookSequence[number], HookDefinition> = Record<$HookSequence[number], HookDefinition>, - // $Result = unknown, - $Options extends InterceptorOptions = InterceptorOptions, -> = { - [$Index in keyof $Pipeline['steps'][number]]: InferPublicHook<$Pipeline['steps'][$Index], $Options> -} +// export type InferPublicHooks< +// $Pipeline extends Pipeline, +// // $HookSequence extends HookSequence, +// // $HookMap extends Record<$HookSequence[number], HookDefinition> = Record<$HookSequence[number], HookDefinition>, +// // $Result = unknown, +// $Options extends InterceptorOptions = InterceptorOptions, +// > = { +// [$Index in keyof $Pipeline['steps'][number]]: InferPublicHook<$Pipeline['steps'][$Index], $Options> +// } -type InferPublicHook< - $Step extends Pipeline.Step, - // $HookSequence extends HookSequence, - // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - // $Result = unknown, - // $Name extends $HookSequence[number] = $HookSequence[number], - $Options extends InterceptorOptions = InterceptorOptions, -> = PublicStep< - ((...args: Parameters<$Step['run']>) => InferPublicHookReturn<$Step, $Options>), - $HookMap[$Name]['input'] -> +// type InferPublicHook< +// $Step extends Step, +// // $HookSequence extends HookSequence, +// // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, +// // $Result = unknown, +// // $Name extends $HookSequence[number] = $HookSequence[number], +// $Options extends InterceptorOptions = InterceptorOptions, +// > = PublicStep< +// ((...args: Parameters<$Step['run']>) => InferPublicHookReturn<$Step, $Options>), +// $HookMap[$Name]['input'] +// > // & (<$$Input extends $HookMap[$Name]['input']>( // input?: // InferHookPrivateInput<$HookSequence,$HookMap,$Name> @@ -35,44 +35,44 @@ type InferPublicHook< // input: $HookMap[$Name]['input'] // } -// dprint-ignore -type InferPublicHookReturn< - $Step extends Pipeline.Step, - // $HookSequence extends HookSequence, - // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, - // $Result = unknown, - // $Name extends $HookSequence[number] = $HookSequence[number], - $Options extends InterceptorOptions = InterceptorOptions, -> = Promise< - | ($Options['retrying'] extends true ? Error : never) - | (IsLastValue<$Name, $HookSequence> extends true - ? $Result - : { - [$NameNext in FindValueAfter<$Name, $HookSequence>]: InferPublicHook< - $HookSequence, - $HookMap, - $Result, - $NameNext - > - } - ) -> +// // dprint-ignore +// type InferPublicHookReturn< +// $Step extends Step, +// // $HookSequence extends HookSequence, +// // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, +// // $Result = unknown, +// // $Name extends $HookSequence[number] = $HookSequence[number], +// $Options extends InterceptorOptions = InterceptorOptions, +// > = Promise< +// | ($Options['retrying'] extends true ? Error : never) +// | (IsLastValue<$Name, $HookSequence> extends true +// ? $Result +// : { +// [$NameNext in FindValueAfter<$Name, $HookSequence>]: InferPublicHook< +// $HookSequence, +// $HookMap, +// $Result, +// $NameNext +// > +// } +// ) +// > -type SlotInputify<$Slots extends Record any>> = { - [K in keyof $Slots]?: SlotInput<$Slots[K]> -} +// type SlotInputify<$Slots extends Record any>> = { +// [K in keyof $Slots]?: SlotInput<$Slots[K]> +// } -type SlotInput any> = (...args: Parameters) => ReturnType | undefined +// type SlotInput any> = (...args: Parameters) => ReturnType | undefined -const hookSymbol = Symbol(`hook`) +const stepTriggerSymbol = Symbol(`hook`) -type HookSymbol = typeof hookSymbol +type StepTriggerSymbol = typeof stepTriggerSymbol -export type SomePublicStepEnvelope = { +export type SomeStepTriggerEnvelope = { [name: string]: PublicStep } -export const createPublicHook = <$OriginalInput, $Fn extends PublicHookFn>( +export const createStepTrigger = <$OriginalInput, $Fn extends PublicHookFn>( originalInput: $OriginalInput, fn: $Fn, ): PublicStep<$Fn> => { @@ -89,7 +89,7 @@ export type PublicStep< > = & $Fn & { - [hookSymbol]: HookSymbol + [stepTriggerSymbol]: StepTriggerSymbol // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. // E.g. adding `| unknown` would destroy the knowledge of hook envelope case // todo this is not strictly true, it could also be the final result diff --git a/src/lib/anyware/run/OptimizedPipeline.ts b/src/lib/anyware/run/OptimizedPipeline.ts new file mode 100644 index 000000000..12e6bb84e --- /dev/null +++ b/src/lib/anyware/run/OptimizedPipeline.ts @@ -0,0 +1,12 @@ +import type { Pipeline } from '../Pipeline/__.js' +import type { Step } from '../Step/__.js' + +export type StepsIndex = Map + +export interface OptimizedPipeline extends Pipeline { + stepsIndex: StepsIndex +} +export const optimizePipeline = (pipeline: Pipeline): OptimizedPipeline => { + const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) + return { ...pipeline, stepsIndex } +} diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index 99bb15782..0bf981018 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -2,6 +2,7 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' import type { Step } from '../Step/__.js' +import type { StepsIndex } from './OptimizedPipeline.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< 'ErrorGraffleInterceptorEntryHook', @@ -23,10 +24,10 @@ export const InterceptorEntryHookIssue = { export type InterceptorEntryHookIssue = typeof InterceptorEntryHookIssue[keyof typeof InterceptorEntryHookIssue] -export const getEntrypoint = ( - hookNames: readonly string[], +export const getEntryStep = ( + stepsIndex: StepsIndex, interceptor: NonRetryingInterceptorInput, -): ErrorAnywareInterceptorEntrypoint | Step.Name => { +): ErrorAnywareInterceptorEntrypoint | Step => { const x = analyzeFunction(interceptor) if (x.parameters.length > 1) { return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.multipleParameters }) @@ -41,16 +42,23 @@ export const getEntrypoint = ( if (p.names.length === 0) { return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.destructuredWithoutEntryHook }) } - const hooks = p.names.filter(_ => hookNames.includes(_ as any)) + const steps = p.names.filter(_ => stepsIndex.has(_)) - if (hooks.length > 1) { + if (steps.length > 1) { return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.multipleDestructuredHookNames }) } - const hook = hooks[0] - if (!hook) { + const stepName = steps[0] + + if (!stepName) { + // todo: destructured with invalid names + return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.multipleDestructuredHookNames }) + } + + const step = stepsIndex.get(stepName) + if (!step) { return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.destructuredWithoutEntryHook }) } else { - return hook + return step } } } diff --git a/src/lib/anyware/run/runPipeline.ts b/src/lib/anyware/run/runPipeline.ts index 2cea77779..0184481a2 100644 --- a/src/lib/anyware/run/runPipeline.ts +++ b/src/lib/anyware/run/runPipeline.ts @@ -3,49 +3,56 @@ import { ContextualError } from '../../errors/ContextualError.js' import { casesExhausted, createDeferred, debug } from '../../prelude.js' import type { HookResult, HookResultErrorAsync } from '../hook/private.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -import type { Pipeline } from '../Pipeline/__.js' +import type { Step } from '../Step/__.js' +import type { OptimizedPipeline } from './OptimizedPipeline.js' import { createResultEnvelope } from './resultEnvelope.js' import type { ResultEnvelop } from './resultEnvelope.js' -import { runHook } from './runHook.js' +import { runStep } from './runStep.js' export const defaultFunctionName = `anonymous` -interface Input { - pipeline: Pipeline - hookNamesOrderedBySequence: readonly string[] - originalInputOrResult: unknown - interceptorsStack: readonly InterceptorGeneric[] - asyncErrorDeferred: HookResultErrorAsync - previous: object -} - export const runPipeline = async ( - { pipeline, hookNamesOrderedBySequence, originalInputOrResult, interceptorsStack, asyncErrorDeferred, previous }: - Input, + { + pipeline, + stepsToProcess, + originalInputOrResult, + interceptorsStack, + asyncErrorDeferred, + previousStepsCompleted, + }: { + pipeline: OptimizedPipeline + stepsToProcess: readonly Step[] + originalInputOrResult: unknown + interceptorsStack: readonly InterceptorGeneric[] + asyncErrorDeferred: HookResultErrorAsync + previousStepsCompleted: object + }, ): Promise => { - const [hookName, ...hookNamesRest] = hookNamesOrderedBySequence + const [stepToProcess, ...stepsRestToProcess] = stepsToProcess - if (!hookName) { + if (!stepToProcess) { debug(`pipeline: ending`) const result = await runPipelineEnd({ interceptorsStack, result: originalInputOrResult }) debug(`pipeline: returning`) return createResultEnvelope(result) } - debug(`hook ${hookName}: start`) + debug(`hook ${stepToProcess.name}: start`) const done = createDeferred({ strict: false }) - void runHook({ + // We do not await the step runner here. + // Instead we work with a deferred passed to it. + void runStep({ pipeline, - name: hookName, + name: stepToProcess.name, done: done.resolve, inputOriginalOrFromExtension: originalInputOrResult as object, - previous, + previousStepsCompleted, interceptorsStack, asyncErrorDeferred, customSlots: {}, - nextExtensionsStack: [], + nextInterceptorsStack: [], }) const signal = await Promise.race( @@ -55,18 +62,18 @@ export const runPipeline = async ( switch (signal.type) { case `completed`: { const { result, effectiveInput, nextExtensionsStack } = signal - const nextPrevious = { - ...previous, - [hookName]: { + const nextPreviousStepsCompleted = { + ...previousStepsCompleted, + [stepToProcess.name]: { input: effectiveInput, }, } return await runPipeline({ pipeline, - hookNamesOrderedBySequence: hookNamesRest, + stepsToProcess: stepsRestToProcess, originalInputOrResult: result, interceptorsStack: nextExtensionsStack, - previous: nextPrevious, + previousStepsCompleted: nextPreviousStepsCompleted, asyncErrorDeferred, }) } @@ -79,16 +86,12 @@ export const runPipeline = async ( debug(`signal: error`) signal - if (pipeline.passthroughErrorWith) { - if (pipeline.passthroughErrorWith(signal)) { - return signal.error as any // todo change return type to be unknown since this function could permit anything? - } + if (pipeline.config.passthroughErrorWith?.(signal)) { + return signal.error as any // todo change return type to be unknown since this function could permit anything? } - if (pipeline.passthroughErrorInstanceOf) { - if (pipeline.passthroughErrorInstanceOf.some(_ => signal.error instanceof _)) { - return signal.error as any // todo change return type to include object... given this instanceof permits that? - } + if (pipeline.config.passthroughErrorInstanceOf.some(_ => signal.error instanceof _)) { + return signal.error as any // todo change return type to include object... given this instanceof permits that? } const wasAsync = asyncErrorDeferred.isResolved() diff --git a/src/lib/anyware/run/runHook.ts b/src/lib/anyware/run/runStep.ts similarity index 78% rename from src/lib/anyware/run/runHook.ts rename to src/lib/anyware/run/runStep.ts index 0433f0fe2..f726f12c3 100644 --- a/src/lib/anyware/run/runHook.ts +++ b/src/lib/anyware/run/runStep.ts @@ -1,59 +1,57 @@ import { Errors } from '../../errors/__.js' import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../../prelude.js' import type { HookResult, HookResultErrorAsync, Slots } from '../hook/private.js' -import { createPublicHook, type SomePublicStepEnvelope } from '../hook/public.js' +import { createStepTrigger, type SomeStepTriggerEnvelope } from '../hook/public.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -import type { Pipeline } from '../Pipeline/__.js' +import type { OptimizedPipeline } from './OptimizedPipeline.js' import type { ResultEnvelop } from './resultEnvelope.js' type HookDoneResolver = (input: HookResult) => void -interface Input { - pipeline: Pipeline - name: string - done: HookDoneResolver - inputOriginalOrFromExtension: object - /** - * Information about previous hook executions, like what their input was. - */ - previous: object - customSlots: Slots - /** - * The extensions that are at this hook awaiting. - */ - interceptorsStack: readonly InterceptorGeneric[] - /** - * The extensions that have advanced past this hook, to their next hook, - * and are now awaiting. - * - * @remarks every extension popped off the stack is added here (except those - * that short-circuit the pipeline or enter passthrough mode). - */ - nextExtensionsStack: readonly InterceptorGeneric[] - asyncErrorDeferred: HookResultErrorAsync -} - const createExecutableChunk = <$Extension extends InterceptorGeneric>(extension: $Extension) => ({ ...extension, - currentChunk: createDeferred(), + currentChunk: createDeferred(), }) -export const runHook = async ( +export const runStep = async ( { pipeline, name, done, inputOriginalOrFromExtension, - previous, + previousStepsCompleted, interceptorsStack, - nextExtensionsStack, + nextInterceptorsStack, asyncErrorDeferred, customSlots, - }: Input, + }: { + pipeline: OptimizedPipeline + name: string + done: HookDoneResolver + inputOriginalOrFromExtension: object + /** + * Information about previous hook executions, like what their input was. + */ + previousStepsCompleted: object + customSlots: Slots + /** + * The extensions that are at this hook awaiting. + */ + interceptorsStack: readonly InterceptorGeneric[] + /** + * The extensions that have advanced past this hook, to their next hook, + * and are now awaiting. + * + * @remarks every extension popped off the stack is added here (except those + * that short-circuit the pipeline or enter passthrough mode). + */ + nextInterceptorsStack: readonly InterceptorGeneric[] + asyncErrorDeferred: HookResultErrorAsync + }, ) => { - const debugHook = debugSub(`hook ${name}:`) + const debugHook = debugSub(`step ${name}:`) - debugHook(`advance to next extension`) + debugHook(`advance to next interceptor`) const [extension, ...extensionsStackRest] = interceptorsStack const isLastExtension = extensionsStackRest.length === 0 @@ -81,7 +79,7 @@ export const runHook = async ( debugExtension(`start`) let hookFailed = false - const hook = createPublicHook(inputOriginalOrFromExtension, (extensionInput) => { + const hook = createStepTrigger(inputOriginalOrFromExtension, (extensionInput) => { debugExtension(`extension calls this hook`, extensionInput) const inputResolved = extensionInput?.input ?? inputOriginalOrFromExtension @@ -125,42 +123,42 @@ export const runHook = async ( } else { debugExtension(`execute branch: retry`) const extensionRetry = createExecutableChunk(extension) - void runHook({ + void runStep({ pipeline, name, done, - previous, + previousStepsCompleted, inputOriginalOrFromExtension, asyncErrorDeferred, interceptorsStack: [extensionRetry], - nextExtensionsStack, + nextInterceptorsStack, customSlots: customSlotsResolved, }) return extensionRetry.currentChunk.promise.then(async (envelope) => { - const envelop_ = envelope as SomePublicStepEnvelope // todo ... better way? + const envelop_ = envelope as SomeStepTriggerEnvelope // todo ... better way? const hook = envelop_[name] // as (params:{input:object;previous:object;using:Slots}) => if (!hook) throw new Error(`Hook not found in envelope: ${name}`) // todo use inputResolved ? const result = await hook({ ...extensionInput, input: extensionInput?.input ?? inputOriginalOrFromExtension, - }) as Promise + }) as Promise return result }) } } else { const extensionWithNextChunk = createExecutableChunk(extension) - const nextNextHookStack = [...nextExtensionsStack, extensionWithNextChunk] // tempting to mutate here but simpler to think about as copy. + const nextNextHookStack = [...nextInterceptorsStack, extensionWithNextChunk] // tempting to mutate here but simpler to think about as copy. hookInvokedDeferred.resolve(true) - void runHook({ + void runStep({ pipeline, name, done, - previous, + previousStepsCompleted, asyncErrorDeferred, inputOriginalOrFromExtension: inputResolved, interceptorsStack: extensionsStackRest, - nextExtensionsStack: nextNextHookStack, + nextInterceptorsStack: nextNextHookStack, customSlots: customSlotsResolved, }) @@ -207,15 +205,15 @@ export const runHook = async ( case `extensionReturned`: { debugExtension(`extension returned`) if (result === envelope) { - void runHook({ + void runStep({ pipeline, name, done, - previous, + previousStepsCompleted, inputOriginalOrFromExtension, asyncErrorDeferred, interceptorsStack: extensionsStackRest, - nextExtensionsStack, + nextInterceptorsStack, customSlots, }) } else { @@ -248,11 +246,11 @@ export const runHook = async ( throw casesExhausted(branch) } } /* reached core for this hook */ else { - debugHook(`no more extensions to advance, run implementation`) + debugHook(`no more interceptors to advance, run implementation`) - const implementation = pipeline.hooks[name] + const implementation = pipeline.stepsIndex.get(name) if (!implementation) { - throw new Errors.ContextualError(`Implementation not found for hook name ${name}`, { hookName: name }) + throw new Errors.ContextualError(`Implementation not found for step name ${name}`, { hookName: name }) } let result @@ -264,11 +262,11 @@ export const runHook = async ( result = await implementation.run({ input: inputOriginalOrFromExtension, slots: slotsResolved, - previous: previous, + previous: previousStepsCompleted, }) } catch (error) { debugHook(`implementation error`) - const lastExtension = nextExtensionsStack[nextExtensionsStack.length - 1] + const lastExtension = nextInterceptorsStack[nextInterceptorsStack.length - 1] if (lastExtension && lastExtension.retrying) { lastExtension.currentChunk.resolve(errorFromMaybeError(error)) } else { @@ -285,7 +283,7 @@ export const runHook = async ( type: `completed`, result, effectiveInput: inputOriginalOrFromExtension, - nextExtensionsStack: nextExtensionsStack, + nextExtensionsStack: nextInterceptorsStack, }) } } diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 7cbfd898c..62d9bb6cb 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -2,72 +2,50 @@ import { partitionAndAggregateErrors } from '../../errors/_.js' import { Errors } from '../../errors/__.js' import { createDeferred } from '../../prelude.js' import { casesExhausted } from '../../prelude.js' -import type { Private } from '../../private.js' import type { HookResultErrorExtension } from '../hook/private.js' -import type { SomePublicStepEnvelope } from '../hook/public.js' +import type { SomeStepTriggerEnvelope } from '../hook/public.js' import { createRetryingInterceptor, type Interceptor, type InterceptorInput } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Step/__.js' -import { getEntrypoint } from './getEntrypoint.js' +import { getEntryStep } from './getEntrypoint.js' +import type { OptimizedPipeline } from './OptimizedPipeline.js' +import { optimizePipeline } from './OptimizedPipeline.js' import { runPipeline } from './runPipeline.js' -export type Runner<$Pipeline extends Pipeline = Pipeline> = ( - { initialInput, interceptors, options }: { - initialInput: GetInitialPipelineInput<$Pipeline> - interceptors: Interceptor.InferConstructor<$Pipeline>[] - retryingInterceptor?: Interceptor.InferConstructor<$Pipeline, { retrying: true }> - options?: Options - }, -) => Promise['result'] | Errors.ContextualError> - -const resolveOptions = (options?: Options): Config => { - return { - entrypointSelectionMode: options?.entrypointSelectionMode ?? `required`, - } -} - -export type Options = { - /** - * @defaultValue `true` - */ - entrypointSelectionMode?: 'optional' | 'required' | 'off' -} - -type Config = Required - export const createRunner = - <$Pipeline extends Pipeline>(pipeline: $Pipeline): Runner<$Pipeline> => - async ({ initialInput, interceptors, options, retryingInterceptor }) => { + <$Pipeline extends Pipeline>(pipeline: $Pipeline) => + async ({ initialInput, interceptors, retryingInterceptor }: { + initialInput: $Pipeline['input'] + interceptors: Interceptor.InferConstructor<$Pipeline>[] + retryingInterceptor?: Interceptor.InferConstructor<$Pipeline> + }): Promise | Errors.ContextualError> => { + const optimizedPipeline = optimizePipeline(pipeline) const interceptors_ = retryingInterceptor ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] : interceptors const initialHookStackAndErrors = interceptors_.map(extension => - toInternalInterceptor(pipeline, resolveOptions(options), extension) + toInternalInterceptor(optimizedPipeline, extension) ) const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) if (error) return error const asyncErrorDeferred = createDeferred({ strict: false }) const result = await runPipeline({ - pipeline, - hookNamesOrderedBySequence: pipeline.hookNamesOrderedBySequence, + pipeline: optimizedPipeline, + stepsToProcess: pipeline.steps, originalInputOrResult: initialInput, // todo fix any interceptorsStack: initialHookStack as any, asyncErrorDeferred, - previous: {}, + previousStepsCompleted: {}, }) if (result instanceof Error) return result return result.result as any } -type GetInitialPipelineInput<$Pipeline extends Pipeline> = Private.Get< - $Pipeline ->['hookMap'][Private.Get<$Pipeline>['hookSequence'][0]]['input'] - -const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: InterceptorInput) => { - const currentChunk = createDeferred() +const toInternalInterceptor = (pipeline: OptimizedPipeline, interceptor: InterceptorInput) => { + const currentChunk = createDeferred() const body = createDeferred() const extensionRun = typeof interceptor === `function` ? interceptor : interceptor.run const retrying = typeof interceptor === `function` ? false : interceptor.retrying @@ -80,29 +58,29 @@ const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: } } - const extensionName = extensionRun.name || `anonymous` + const interceptorName = extensionRun.name || `anonymous` - switch (config.entrypointSelectionMode) { + switch (pipeline.config.entrypointSelectionMode) { case `off`: { void currentChunk.promise.then(applyBody) return { - name: extensionName, - entrypoint: pipeline.hookNamesOrderedBySequence[0], // todo non-empty-array data structure + name: interceptorName, + entrypoint: pipeline.steps[0]?.name, body, currentChunk, } } case `optional`: case `required`: { - const entrypoint = getEntrypoint(pipeline.hookNamesOrderedBySequence, extensionRun) - if (entrypoint instanceof Error) { - if (config.entrypointSelectionMode === `required`) { - return entrypoint + const entryStep = getEntryStep(pipeline.stepsIndex, extensionRun) + if (entryStep instanceof Error) { + if (pipeline.config.entrypointSelectionMode === `required`) { + return entryStep } else { void currentChunk.promise.then(applyBody) return { - name: extensionName, - entrypoint: pipeline.hookNamesOrderedBySequence[0], // todo non-empty-array data structure + name: interceptorName, + entrypoint: pipeline.steps[0]?.name, body, currentChunk, } @@ -110,9 +88,9 @@ const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: } const hooksBeforeEntrypoint: Step.Name[] = [] - for (const hookName of pipeline.hookNamesOrderedBySequence) { - if (hookName === entrypoint) break - hooksBeforeEntrypoint.push(hookName) + for (const step of pipeline.steps) { + if (step === entryStep) break + hooksBeforeEntrypoint.push(step.name) } const passthroughs = hooksBeforeEntrypoint.map((hookName) => createPassthrough(hookName)) @@ -124,18 +102,18 @@ const toInternalInterceptor = (pipeline: Pipeline, config: Config, interceptor: return { retrying, - name: extensionName, - entrypoint, + name: interceptorName, + entryStep, body, currentChunk, } } default: - throw casesExhausted(config.entrypointSelectionMode) + throw casesExhausted(pipeline.config.entrypointSelectionMode) } } -const createPassthrough = (hookName: string) => async (hookEnvelope: SomePublicStepEnvelope) => { +const createPassthrough = (hookName: string) => async (hookEnvelope: SomeStepTriggerEnvelope) => { const hook = hookEnvelope[hookName] if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 746e38bcb..0b7738bc2 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -334,6 +334,41 @@ export namespace Tuple { [] export type IndexPlusOne<$Index extends NumberLiteral> = PlusOne<$Index> + + export type GetLastValue = T[MinusOne] + + export type IsLastValue = value extends GetLastValue ? true + : false + + // dprint-ignore + export type findIndexForValue = + findIndexForValue_ + + // dprint-ignore + type findIndexForValue_ = + value extends list[i] + ? i + : findIndexForValue_> + + export type FindValueAfter = + list[PlusOne>] + + // dprint-ignore + export type TakeValuesBefore<$Value, $List extends AnyReadOnly> = + $List extends readonly [infer $ListFirst, ...infer $ListRest] + ? $Value extends $ListFirst + ? [] + : [$ListFirst, ...TakeValuesBefore<$Value, $ListRest>] + : [] + + export type FindValueAfterOr = ConfigManager.OrDefault< + list[PlusOne>], + orValue + > + + type AnyReadOnly = readonly any[] + + type AnyReadOnlyListNonEmpty = readonly [any, ...any[]] } type NumberLiteral = number | `${number}` @@ -366,40 +401,6 @@ export type MinusOne = : n extends 1 ? 0 : never -// dprint-ignore -export type findIndexForValue = - findIndexForValue_ - -// dprint-ignore -type findIndexForValue_ = - value extends list[i] - ? i - : findIndexForValue_> - -export type FindValueAfter = list[PlusOne>] - -// dprint-ignore -export type TakeValuesBefore<$Value, $List extends AnyReadOnlyList> = - $List extends readonly [infer $ListFirst, ...infer $ListRest] - ? $Value extends $ListFirst - ? [] - : [$ListFirst, ...TakeValuesBefore<$Value, $ListRest>] - : [] - -type AnyReadOnlyListNonEmpty = readonly [any, ...any[]] -type AnyReadOnlyList = readonly [...any[]] - -export type ValueOr = value extends undefined ? orValue : value - -export type FindValueAfterOr = ValueOr< - list[PlusOne>], - orValue -> - -export type GetLastValue = T[MinusOne] - -export type IsLastValue = value extends GetLastValue ? true : false - export type Include = T extends U ? T : never export const partitionErrors = (array: T[]): [Exclude[], Include[]] => { From 253afa7f2298208e86632a9a21378e29ab001e34 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 6 Nov 2024 21:15:20 -0500 Subject: [PATCH 14/36] fixes --- src/lib/anyware/Pipeline/builder.test-d.ts | 7 ++++--- src/lib/anyware/Pipeline/builder.ts | 16 ++++++---------- src/lib/anyware/__.test-helpers.ts | 6 ++---- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/Pipeline/builder.test-d.ts index d6a06f227..c80b4bda3 100644 --- a/src/lib/anyware/Pipeline/builder.test-d.ts +++ b/src/lib/anyware/Pipeline/builder.test-d.ts @@ -2,11 +2,12 @@ import { expectTypeOf, test } from 'vitest' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import { Pipeline } from './__.js' +import type { Config } from './builder.js' const p0 = Pipeline.create() test(`initial context`, () => { - expectTypeOf(p0.context).toEqualTypeOf<{ input: initialInput; output: object; steps: []; stepsIndex: {} }>() + expectTypeOf(p0.context).toEqualTypeOf<{ input: initialInput; steps: []; config: Config }>() }) test(`first step definition`, () => { @@ -32,8 +33,8 @@ test(`second step definition`, () => { expectTypeOf(p1.context).toMatchTypeOf< { input: initialInput - output: object steps: [{ name: 'a'; slots: undefined; run: any }] + config: Config } >() }) @@ -60,7 +61,7 @@ test(`step definition with slots`, () => { expectTypeOf(p1.context).toMatchTypeOf< { input: initialInput - output: object + config: Config steps: [{ name: 'a' slots: slots diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index fc7abaa0b..20f5ef14d 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,11 +1,9 @@ import type { ConfigManager } from '../../config-manager/__.js' -import { type GetLastValue, type Tuple } from '../../prelude.js' +import { type Tuple } from '../../prelude.js' import type { HookResultError } from '../hook/private.js' import type { Step } from '../Step/__.js' import type { Pipeline } from './__.js' -// export { type HookDefinitionMap } from '../hook/definition.js' - export interface Context { input: object steps: Step[] @@ -32,7 +30,7 @@ export type GetAwaitedResult<$Pipeline extends Pipeline> = Awaited = $Pipeline['steps'] extends [any, ...any[]] - ? Step.GetResult> + ? Step.GetResult> : $Pipeline['input'] // dprint-ignore @@ -64,7 +62,7 @@ type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< // dprint-ignore type GetNextStepParameterInput<$Pipeline extends Pipeline> = $Pipeline['steps'] extends [any, ...any[]] - ? Awaited>> + ? Awaited>> : $Pipeline['input'] export interface Builder<$Context extends Context = Context> { @@ -115,7 +113,7 @@ export interface Builder<$Context extends Context = Context> { export type Infer<$Builder extends Builder> = $Builder['context'] -interface Options { +export interface Options { /** * @defaultValue `required` */ @@ -138,7 +136,7 @@ interface Options { passthroughErrorWith?: null | ((signal: HookResultError) => boolean) } -type Config = Required +export type Config = Required /** * TODO @@ -146,11 +144,9 @@ type Config = Required export const create = <$Input extends object>(options?: Options): Builder<{ input: $Input steps: [] - stepsIndex: {} - output: object config: Config }> => { - const config = resolveOptions(options) + const _config = resolveOptions(options) return undefined as any } diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index 1cfeaa3a0..6ac01f25e 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -3,7 +3,6 @@ import { beforeEach, vi } from 'vitest' import type { Tuple } from '../prelude.js' import { Pipeline } from './_.js' import type { InterceptorInput } from './Interceptor/Interceptor.js' -import type { Options } from './run/runner.js' export const initialInput = { x: 1 } as const export type initialInput = typeof initialInput @@ -74,15 +73,14 @@ beforeEach(() => { builder = createPipeline() }) -export const runWithOptions = (options: Options = {}) => async (...interceptors: InterceptorInput[]) => { +export const runWithOptions = () => async (...interceptors: InterceptorInput[]) => { return await Pipeline.run(builder, { initialInput, // @ts-expect-error fixme interceptors, - options, }) } -export const run = async (...extensions: InterceptorInput[]) => runWithOptions({})(...extensions) +export const run = async (...extensions: InterceptorInput[]) => runWithOptions()(...extensions) export const oops = new Error(`oops`) From 44e68ec95c89956d5cec385626284e9a659f604f Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 6 Nov 2024 21:35:23 -0500 Subject: [PATCH 15/36] break parts aprt --- .../anyware/Interceptor/Interceptor.test-d.ts | 6 +- src/lib/anyware/Interceptor/Interceptor.ts | 10 +- src/lib/anyware/Pipeline/builder.ts | 6 +- src/lib/anyware/Step.ts | 21 ++++ src/lib/anyware/Step/Step.ts | 7 -- src/lib/anyware/Step/__.ts | 11 -- src/lib/anyware/StepResult.ts | 49 +++++++++ src/lib/anyware/StepTrigger.ts | 28 +++++ src/lib/anyware/StepTriggerEnvelope.ts | 5 + src/lib/anyware/__.test-d.ts | 2 +- src/lib/anyware/hook/private.ts | 81 -------------- src/lib/anyware/hook/public.ts | 104 ------------------ src/lib/anyware/run/OptimizedPipeline.ts | 2 +- src/lib/anyware/run/getEntrypoint.ts | 2 +- src/lib/anyware/run/runPipeline.ts | 8 +- src/lib/anyware/run/runStep.ts | 27 ++--- src/lib/anyware/run/runner.ts | 12 +- 17 files changed, 141 insertions(+), 240 deletions(-) create mode 100644 src/lib/anyware/Step.ts delete mode 100644 src/lib/anyware/Step/Step.ts delete mode 100644 src/lib/anyware/Step/__.ts create mode 100644 src/lib/anyware/StepResult.ts create mode 100644 src/lib/anyware/StepTrigger.ts create mode 100644 src/lib/anyware/StepTriggerEnvelope.ts delete mode 100644 src/lib/anyware/hook/private.ts delete mode 100644 src/lib/anyware/hook/public.ts diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 4a73c44b1..fcd49f6f6 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -3,8 +3,8 @@ import { _, type ExcludeUndefined } from '../../prelude.js' import { type Interceptor, Pipeline } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' -import type { SomeStepTriggerEnvelope } from '../hook/public.js' import type { Builder } from '../Pipeline/builder.js' +import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' const p0 = Pipeline.create() @@ -77,12 +77,12 @@ describe(`interceptor constructor`, () => { // test(`can return pipeline output or a step envelope`, () => { const p = p0.step({ name: `a`, run: () => results.a }) - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index f9f5483e5..4fd2a8644 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,8 +1,8 @@ import type { Simplify } from 'type-fest' import type { Deferred, Func, MaybePromise } from '../../prelude.js' -import type { SomeStepTriggerEnvelope } from '../hook/public.js' import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Pipeline/builder.js' +import type { Step } from '../Step.js' +import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' export type InterceptorOptions = { retrying: boolean @@ -25,7 +25,7 @@ export namespace Interceptor { steps: Simplify>, ): Promise< | Pipeline.GetAwaitedResult<$Pipeline> - | SomeStepTriggerEnvelope + | StepTriggerEnvelope > } @@ -79,7 +79,7 @@ export type NonRetryingInterceptor = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export type RetryingInterceptor = { @@ -87,7 +87,7 @@ export type RetryingInterceptor = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export const createRetryingInterceptor = (extension: NonRetryingInterceptorInput): RetryingInterceptorInput => { diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 20f5ef14d..7af002bfe 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,7 +1,7 @@ import type { ConfigManager } from '../../config-manager/__.js' import { type Tuple } from '../../prelude.js' -import type { HookResultError } from '../hook/private.js' -import type { Step } from '../Step/__.js' +import type { Step } from '../Step.js' +import type { StepResultError } from '../StepResult.js' import type { Pipeline } from './__.js' export interface Context { @@ -133,7 +133,7 @@ export interface Options { * This can be useful when there are known kinds of errors such as Abort Errors from AbortController * which are actually a signaling mechanism. */ - passthroughErrorWith?: null | ((signal: HookResultError) => boolean) + passthroughErrorWith?: null | ((signal: StepResultError) => boolean) } export type Config = Required diff --git a/src/lib/anyware/Step.ts b/src/lib/anyware/Step.ts new file mode 100644 index 000000000..010db5147 --- /dev/null +++ b/src/lib/anyware/Step.ts @@ -0,0 +1,21 @@ +import type { SomeFunction } from '../prelude.js' + +export interface Step< + $Name extends string = string, +> { + name: $Name + slots: Step.Slots + input: any + output: any + run: (params: any) => any +} + +export namespace Step { + export type Slots = Record + + export type Name = string + + export type GetAwaitedResult<$Step extends Step> = Awaited> + + export type GetResult<$Step extends Step> = ReturnType<$Step['run']> +} diff --git a/src/lib/anyware/Step/Step.ts b/src/lib/anyware/Step/Step.ts deleted file mode 100644 index af361f78e..000000000 --- a/src/lib/anyware/Step/Step.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Step } from './__.js' - -export type Name = string - -export type GetAwaitedResult<$Step extends Step> = Awaited> - -export type GetResult<$Step extends Step> = ReturnType<$Step['run']> diff --git a/src/lib/anyware/Step/__.ts b/src/lib/anyware/Step/__.ts deleted file mode 100644 index 841ede02d..000000000 --- a/src/lib/anyware/Step/__.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * as Step from './Step.js' - -export interface Step< - $Name extends string = string, -> { - name: $Name - slots: object | undefined - input: any - output: any - run: (params: any) => any -} diff --git a/src/lib/anyware/StepResult.ts b/src/lib/anyware/StepResult.ts new file mode 100644 index 000000000..0b1f6f0a6 --- /dev/null +++ b/src/lib/anyware/StepResult.ts @@ -0,0 +1,49 @@ +import type { Errors } from '../errors/__.js' +import type { Deferred } from '../prelude.js' +import type { InterceptorGeneric } from './Interceptor/Interceptor.js' + +export type StepResult = + | StepResultCompleted + | StepResultShortCircuited + | StepResultErrorUser + | StepResultErrorImplementation + | StepResultErrorExtension + +export interface StepResultShortCircuited { + type: 'shortCircuited' + result: unknown +} + +export interface StepResultCompleted { + type: 'completed' + effectiveInput: object + result: unknown + nextExtensionsStack: readonly InterceptorGeneric[] +} + +export type StepResultError = StepResultErrorExtension | StepResultErrorImplementation | StepResultErrorUser + +export interface StepResultErrorUser { + type: 'error' + hookName: string + source: 'user' + error: Errors.ContextualError + extensionName: string +} + +export interface StepResultErrorExtension { + type: 'error' + hookName: string + source: 'extension' + error: Error + interceptorName: string +} + +export interface StepResultErrorImplementation { + type: 'error' + hookName: string + source: 'implementation' + error: Error +} + +export type StepResultErrorAsync = Deferred diff --git a/src/lib/anyware/StepTrigger.ts b/src/lib/anyware/StepTrigger.ts new file mode 100644 index 000000000..547eb575f --- /dev/null +++ b/src/lib/anyware/StepTrigger.ts @@ -0,0 +1,28 @@ +const stepTriggerSymbol = Symbol(`hook`) + +type StepTriggerSymbol = typeof stepTriggerSymbol + +export namespace StepTrigger { + export const create = <$OriginalInput, $Fn extends StepTriggerBase>( + originalInput: $OriginalInput, + fn: $Fn, + ): StepTrigger<$Fn> => { + // ): $Hook & { input: $OriginalInput } => { + // @ts-expect-error + fn.input = originalInput + // @ts-expect-error + return fn + } +} + +export interface StepTrigger< + $OriginalInput extends object = object, // Exclude[0], undefined>['input'], +> extends StepTriggerBase { + [stepTriggerSymbol]: StepTriggerSymbol + // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. + // E.g. adding `| unknown` would destroy the knowledge of hook envelope case + // todo this is not strictly true, it could also be the final result + input: $OriginalInput +} + +type StepTriggerBase = (input?: { input?: any; using?: any }) => any diff --git a/src/lib/anyware/StepTriggerEnvelope.ts b/src/lib/anyware/StepTriggerEnvelope.ts new file mode 100644 index 000000000..ee252d243 --- /dev/null +++ b/src/lib/anyware/StepTriggerEnvelope.ts @@ -0,0 +1,5 @@ +import type { StepTrigger } from './StepTrigger.js' + +export type StepTriggerEnvelope = { + [name: string]: StepTrigger +} diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts index 16aa76fb3..5c73f6a0e 100644 --- a/src/lib/anyware/__.test-d.ts +++ b/src/lib/anyware/__.test-d.ts @@ -5,7 +5,7 @@ import { assertEqual } from '../assert-equal.js' import { ContextualError } from '../errors/ContextualError.js' import { type MaybePromise } from '../prelude.js' import { Anyware } from './__.js' -import type { PublicStep } from './hook/public.js' +import type { StepTrigger } from './StepTrigger.js' // describe('without slots', () => { // test('run', () => { diff --git a/src/lib/anyware/hook/private.ts b/src/lib/anyware/hook/private.ts deleted file mode 100644 index ea4fa3089..000000000 --- a/src/lib/anyware/hook/private.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Errors } from '../../errors/__.js' -import type { Deferred, MaybePromise, SomeFunction } from '../../prelude.js' -import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -// import type { HookDefinitionMap, HookSequence } from './definition.js' - -// export type InferPrivateHookInput< -// $HookSequence extends HookSequence, -// $HookMap extends HookDefinitionMap<$HookSequence>, -// $HookName extends string, -// > = HookPrivateInput< -// $HookMap[$HookName]['input'], -// $HookMap[$HookName]['slots'], -// { -// [$PreviousHookName in TakeValuesBefore<$HookName, $HookSequence>[number]]: { -// input: $HookMap[$PreviousHookName]['input'] -// } -// } -// > - -export type PrivateHook<$Slots extends Slots, $Input extends HookPrivateInput, $Return> = { - slots: $Slots - run: (input: $Input) => MaybePromise<$Return> -} - -export type HookPrivateInput< - $Input extends object | undefined = object | undefined, - $Slots extends Slots | undefined = Slots | undefined, - $Previous extends object = object, -> = { - input: $Input - slots: $Slots - previous: $Previous -} - -export type Slots = Record - -export type HookResult = - | HookResultCompleted - | HookResultShortCircuited - | HookResultErrorUser - | HookResultErrorImplementation - | HookResultErrorExtension - -export interface HookResultShortCircuited { - type: 'shortCircuited' - result: unknown -} - -export interface HookResultCompleted { - type: 'completed' - effectiveInput: object - result: unknown - nextExtensionsStack: readonly InterceptorGeneric[] -} - -export type HookResultError = HookResultErrorExtension | HookResultErrorImplementation | HookResultErrorUser - -export interface HookResultErrorUser { - type: 'error' - hookName: string - source: 'user' - error: Errors.ContextualError - extensionName: string -} - -export interface HookResultErrorExtension { - type: 'error' - hookName: string - source: 'extension' - error: Error - interceptorName: string -} - -export interface HookResultErrorImplementation { - type: 'error' - hookName: string - source: 'implementation' - error: Error -} - -export type HookResultErrorAsync = Deferred diff --git a/src/lib/anyware/hook/public.ts b/src/lib/anyware/hook/public.ts deleted file mode 100644 index ab659562c..000000000 --- a/src/lib/anyware/hook/public.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { InterceptorOptions } from '../Interceptor/Interceptor.js' -import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Step/__.js' - -// export type InferPublicHooks< -// $Pipeline extends Pipeline, -// // $HookSequence extends HookSequence, -// // $HookMap extends Record<$HookSequence[number], HookDefinition> = Record<$HookSequence[number], HookDefinition>, -// // $Result = unknown, -// $Options extends InterceptorOptions = InterceptorOptions, -// > = { -// [$Index in keyof $Pipeline['steps'][number]]: InferPublicHook<$Pipeline['steps'][$Index], $Options> -// } - -// type InferPublicHook< -// $Step extends Step, -// // $HookSequence extends HookSequence, -// // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, -// // $Result = unknown, -// // $Name extends $HookSequence[number] = $HookSequence[number], -// $Options extends InterceptorOptions = InterceptorOptions, -// > = PublicStep< -// ((...args: Parameters<$Step['run']>) => InferPublicHookReturn<$Step, $Options>), -// $HookMap[$Name]['input'] -// > - -// & (<$$Input extends $HookMap[$Name]['input']>( -// input?: // InferHookPrivateInput<$HookSequence,$HookMap,$Name> -// { -// input?: $$Input -// } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), -// ) => PublicHookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>) -// & { -// [hookSymbol]: HookSymbol -// input: $HookMap[$Name]['input'] -// } - -// // dprint-ignore -// type InferPublicHookReturn< -// $Step extends Step, -// // $HookSequence extends HookSequence, -// // $HookMap extends HookDefinitionMap<$HookSequence> = HookDefinitionMap<$HookSequence>, -// // $Result = unknown, -// // $Name extends $HookSequence[number] = $HookSequence[number], -// $Options extends InterceptorOptions = InterceptorOptions, -// > = Promise< -// | ($Options['retrying'] extends true ? Error : never) -// | (IsLastValue<$Name, $HookSequence> extends true -// ? $Result -// : { -// [$NameNext in FindValueAfter<$Name, $HookSequence>]: InferPublicHook< -// $HookSequence, -// $HookMap, -// $Result, -// $NameNext -// > -// } -// ) -// > - -// type SlotInputify<$Slots extends Record any>> = { -// [K in keyof $Slots]?: SlotInput<$Slots[K]> -// } - -// type SlotInput any> = (...args: Parameters) => ReturnType | undefined - -const stepTriggerSymbol = Symbol(`hook`) - -type StepTriggerSymbol = typeof stepTriggerSymbol - -export type SomeStepTriggerEnvelope = { - [name: string]: PublicStep -} - -export const createStepTrigger = <$OriginalInput, $Fn extends PublicHookFn>( - originalInput: $OriginalInput, - fn: $Fn, -): PublicStep<$Fn> => { - // ): $Hook & { input: $OriginalInput } => { - // @ts-expect-error - fn.input = originalInput - // @ts-expect-error - return fn -} - -export type PublicStep< - $Fn extends PublicHookFn = PublicHookFn, - $OriginalInput extends object = object, // Exclude[0], undefined>['input'], -> = - & $Fn - & { - [stepTriggerSymbol]: StepTriggerSymbol - // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. - // E.g. adding `| unknown` would destroy the knowledge of hook envelope case - // todo this is not strictly true, it could also be the final result - input: $OriginalInput - } - -type PublicHookFn = (input?: HookPublicInput) => any - -interface HookPublicInput { - input?: any - using?: any -} diff --git a/src/lib/anyware/run/OptimizedPipeline.ts b/src/lib/anyware/run/OptimizedPipeline.ts index 12e6bb84e..8e849c3ec 100644 --- a/src/lib/anyware/run/OptimizedPipeline.ts +++ b/src/lib/anyware/run/OptimizedPipeline.ts @@ -1,5 +1,5 @@ import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Step/__.js' +import type { Step } from '../Step.js' export type StepsIndex = Map diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index 0bf981018..af9482c66 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,7 +1,7 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' -import type { Step } from '../Step/__.js' +import type { Step } from '../Step.js' import type { StepsIndex } from './OptimizedPipeline.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< diff --git a/src/lib/anyware/run/runPipeline.ts b/src/lib/anyware/run/runPipeline.ts index 0184481a2..b01e52183 100644 --- a/src/lib/anyware/run/runPipeline.ts +++ b/src/lib/anyware/run/runPipeline.ts @@ -1,9 +1,9 @@ import type { Errors } from '../../errors/__.js' import { ContextualError } from '../../errors/ContextualError.js' import { casesExhausted, createDeferred, debug } from '../../prelude.js' -import type { HookResult, HookResultErrorAsync } from '../hook/private.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -import type { Step } from '../Step/__.js' +import type { Step } from '../Step.js' +import type { StepResult, StepResultErrorAsync } from '../StepResult.js' import type { OptimizedPipeline } from './OptimizedPipeline.js' import { createResultEnvelope } from './resultEnvelope.js' import type { ResultEnvelop } from './resultEnvelope.js' @@ -24,7 +24,7 @@ export const runPipeline = async ( stepsToProcess: readonly Step[] originalInputOrResult: unknown interceptorsStack: readonly InterceptorGeneric[] - asyncErrorDeferred: HookResultErrorAsync + asyncErrorDeferred: StepResultErrorAsync previousStepsCompleted: object }, ): Promise => { @@ -39,7 +39,7 @@ export const runPipeline = async ( debug(`hook ${stepToProcess.name}: start`) - const done = createDeferred({ strict: false }) + const done = createDeferred({ strict: false }) // We do not await the step runner here. // Instead we work with a deferred passed to it. diff --git a/src/lib/anyware/run/runStep.ts b/src/lib/anyware/run/runStep.ts index f726f12c3..d8a1c8f04 100644 --- a/src/lib/anyware/run/runStep.ts +++ b/src/lib/anyware/run/runStep.ts @@ -1,16 +1,18 @@ import { Errors } from '../../errors/__.js' import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../../prelude.js' -import type { HookResult, HookResultErrorAsync, Slots } from '../hook/private.js' -import { createStepTrigger, type SomeStepTriggerEnvelope } from '../hook/public.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' +import type { Step } from '../Step.js' +import type { StepResult, StepResultErrorAsync } from '../StepResult.js' +import { StepTrigger } from '../StepTrigger.js' +import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import type { OptimizedPipeline } from './OptimizedPipeline.js' import type { ResultEnvelop } from './resultEnvelope.js' -type HookDoneResolver = (input: HookResult) => void +type HookDoneResolver = (input: StepResult) => void const createExecutableChunk = <$Extension extends InterceptorGeneric>(extension: $Extension) => ({ ...extension, - currentChunk: createDeferred(), + currentChunk: createDeferred(), }) export const runStep = async ( @@ -33,7 +35,7 @@ export const runStep = async ( * Information about previous hook executions, like what their input was. */ previousStepsCompleted: object - customSlots: Slots + customSlots: Step.Slots /** * The extensions that are at this hook awaiting. */ @@ -46,7 +48,7 @@ export const runStep = async ( * that short-circuit the pipeline or enter passthrough mode). */ nextInterceptorsStack: readonly InterceptorGeneric[] - asyncErrorDeferred: HookResultErrorAsync + asyncErrorDeferred: StepResultErrorAsync }, ) => { const debugHook = debugSub(`step ${name}:`) @@ -79,7 +81,7 @@ export const runStep = async ( debugExtension(`start`) let hookFailed = false - const hook = createStepTrigger(inputOriginalOrFromExtension, (extensionInput) => { + const trigger = StepTrigger.create(inputOriginalOrFromExtension, (extensionInput) => { debugExtension(`extension calls this hook`, extensionInput) const inputResolved = extensionInput?.input ?? inputOriginalOrFromExtension @@ -135,14 +137,14 @@ export const runStep = async ( customSlots: customSlotsResolved, }) return extensionRetry.currentChunk.promise.then(async (envelope) => { - const envelop_ = envelope as SomeStepTriggerEnvelope // todo ... better way? + const envelop_ = envelope as StepTriggerEnvelope // todo ... better way? const hook = envelop_[name] // as (params:{input:object;previous:object;using:Slots}) => if (!hook) throw new Error(`Hook not found in envelope: ${name}`) // todo use inputResolved ? const result = await hook({ ...extensionInput, input: extensionInput?.input ?? inputOriginalOrFromExtension, - }) as Promise + }) as Promise return result }) } @@ -175,9 +177,8 @@ export const runStep = async ( // The extension is resumed. It is responsible for calling the next hook. debugExtension(`advance with envelope`) - // @ts-expect-error fixme - const envelope: SomeHookEnvelope = { - [name]: hook, + const envelope: StepTriggerEnvelope = { + [name]: trigger, } extension.currentChunk.resolve(envelope) @@ -256,7 +257,7 @@ export const runStep = async ( let result try { const slotsResolved = { - ...implementation.slots as Slots, // todo is this cast needed, can we Slots type the property? + ...implementation.slots, ...customSlots, } result = await implementation.run({ diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 62d9bb6cb..bf62480f6 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -2,11 +2,11 @@ import { partitionAndAggregateErrors } from '../../errors/_.js' import { Errors } from '../../errors/__.js' import { createDeferred } from '../../prelude.js' import { casesExhausted } from '../../prelude.js' -import type { HookResultErrorExtension } from '../hook/private.js' -import type { SomeStepTriggerEnvelope } from '../hook/public.js' import { createRetryingInterceptor, type Interceptor, type InterceptorInput } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Step/__.js' +import type { Step } from '../Step.js' +import type { StepResultErrorExtension } from '../StepResult.js' +import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import { getEntryStep } from './getEntrypoint.js' import type { OptimizedPipeline } from './OptimizedPipeline.js' import { optimizePipeline } from './OptimizedPipeline.js' @@ -29,7 +29,7 @@ export const createRunner = const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) if (error) return error - const asyncErrorDeferred = createDeferred({ strict: false }) + const asyncErrorDeferred = createDeferred({ strict: false }) const result = await runPipeline({ pipeline: optimizedPipeline, stepsToProcess: pipeline.steps, @@ -45,7 +45,7 @@ export const createRunner = } const toInternalInterceptor = (pipeline: OptimizedPipeline, interceptor: InterceptorInput) => { - const currentChunk = createDeferred() + const currentChunk = createDeferred() const body = createDeferred() const extensionRun = typeof interceptor === `function` ? interceptor : interceptor.run const retrying = typeof interceptor === `function` ? false : interceptor.retrying @@ -113,7 +113,7 @@ const toInternalInterceptor = (pipeline: OptimizedPipeline, interceptor: Interce } } -const createPassthrough = (hookName: string) => async (hookEnvelope: SomeStepTriggerEnvelope) => { +const createPassthrough = (hookName: string) => async (hookEnvelope: StepTriggerEnvelope) => { const hook = hookEnvelope[hookName] if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) From 2ed8cc2ac3204f92fb1dc47805bdd7dc1b9f492c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 7 Nov 2024 09:48:32 -0500 Subject: [PATCH 16/36] wip --- src/client/builderExtensions/anyware.ts | 2 +- src/client/client.transport-http.test.ts | 10 +- src/client/gql/gql.ts | 2 +- .../requestMethods/requestMethods.ts | 5 +- src/lib/anyware/Interceptor/Interceptor.ts | 31 +- src/lib/anyware/Pipeline/FromSteps.ts | 4 + src/lib/anyware/Pipeline/_.ts | 2 + src/lib/anyware/Pipeline/builder.ts | 4 +- src/lib/anyware/Pipeline/createFromType.ts | 5 + src/lib/anyware/Step.ts | 41 +- src/lib/anyware/StepTrigger.ts | 66 ++- src/lib/anyware/__.test-helpers.ts | 30 +- src/lib/anyware/__.test.ts | 100 ++--- src/lib/prelude.test-d.ts | 8 +- src/lib/prelude.ts | 4 +- src/requestPipeline/RequestPipeline.ts | 394 +++++++++--------- tests/_/SpyExtension.ts | 6 +- tests/_/helpers.ts | 2 - 18 files changed, 387 insertions(+), 329 deletions(-) create mode 100644 src/lib/anyware/Pipeline/FromSteps.ts create mode 100644 src/lib/anyware/Pipeline/createFromType.ts diff --git a/src/client/builderExtensions/anyware.ts b/src/client/builderExtensions/anyware.ts index b025f2193..d0d8ac4dc 100644 --- a/src/client/builderExtensions/anyware.ts +++ b/src/client/builderExtensions/anyware.ts @@ -16,7 +16,7 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters + RequestPipeline // <$Arguments['context']['config']> >, ) => Builder.Definition.MaterializeWithNewContext<$Arguments['chain'], $Arguments['context']> } diff --git a/src/client/client.transport-http.test.ts b/src/client/client.transport-http.test.ts index 733465b5f..ca3c79c75 100644 --- a/src/client/client.transport-http.test.ts +++ b/src/client/client.transport-http.test.ts @@ -5,20 +5,22 @@ import { Graffle as Pokemon } from '../../tests/_/schemas/pokemon/graffle/__.js' import { schema as schemaPokemon } from '../../tests/_/schemas/pokemon/schema.js' import { Graffle } from '../entrypoints/main.js' import { ACCEPT_REC, CONTENT_TYPE_REC } from '../lib/grafaid/http/http.js' -import type { CoreExchangeGetRequest, CoreExchangePostRequest } from '../requestPipeline/types.js' -import { Transport } from '../types/Transport.js' +import type { RequestPipeline } from '../requestPipeline/__.js' +import { Transport, type TransportHttp } from '../types/Transport.js' const schema = new URL(`https://foo.io/api/graphql`) test(`anyware hooks are typed to http transport`, () => { Graffle.create({ schema }).anyware(async ({ encode }) => { - expectTypeOf(encode.input.transportType).toEqualTypeOf(Transport.http) + expectTypeOf(encode.input.transportType).toEqualTypeOf() const { pack } = await encode() expectTypeOf(pack.input.transportType).toEqualTypeOf(Transport.http) const { exchange } = await pack() expectTypeOf(exchange.input.transportType).toEqualTypeOf(Transport.http) // todo we can statically track the method mode like we do the transport mode - expectTypeOf(exchange.input.request).toEqualTypeOf() + expectTypeOf(exchange.input.request).toEqualTypeOf< + RequestPipeline.Steps.CoreExchangePostRequest | RequestPipeline.Steps.CoreExchangeGetRequest + >() const { unpack } = await exchange() expectTypeOf(unpack.input.transportType).toEqualTypeOf(Transport.http) expectTypeOf(unpack.input.response).toEqualTypeOf() diff --git a/src/client/gql/gql.ts b/src/client/gql/gql.ts index 57168d17b..4c3e4cdf5 100644 --- a/src/client/gql/gql.ts +++ b/src/client/gql/gql.ts @@ -72,7 +72,7 @@ export const builderExtensionGql = Builder.Extension.create schema, // request, request: analyzedRequest, - } as RequestPipeline.Hooks.HookDefEncode['input'] + } as RequestPipeline.Steps.HookDefEncode['input'] const result = await Anyware.Pipeline.run(RequestPipeline, { initialInput, diff --git a/src/documentBuilder/requestMethods/requestMethods.ts b/src/documentBuilder/requestMethods/requestMethods.ts index 13205bffc..5b4d76904 100644 --- a/src/documentBuilder/requestMethods/requestMethods.ts +++ b/src/documentBuilder/requestMethods/requestMethods.ts @@ -4,6 +4,7 @@ import { type Context } from '../../client/context.js' import { handleOutput } from '../../client/handleOutput.js' import type { Config } from '../../client/Settings/Config.js' import type { TypeFunction } from '../../entrypoints/utilities-for-generated.js' +import { Anyware } from '../../lib/anyware/__.js' import { Builder } from '../../lib/builder/__.js' import type { Grafaid } from '../../lib/grafaid/__.js' import { getOperationDefinition } from '../../lib/grafaid/document.js' @@ -135,9 +136,9 @@ const executeDocument = async ( url, schema, request, - } as RequestPipeline.Hooks.HookDefEncode['input'] + } as RequestPipeline.Steps.HookDefEncode['input'] - const result = await RequestPipeline.RequestPipeline.run({ + const result = await Anyware.Pipeline.run(RequestPipeline, { initialInput, // retryingExtension: state.retry as any, interceptors: state.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index 4fd2a8644..71cbe3512 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,7 +1,8 @@ import type { Simplify } from 'type-fest' -import type { Deferred, Func, MaybePromise } from '../../prelude.js' +import type { Deferred, MaybePromise } from '../../prelude.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Step.js' +import type { StepTrigger } from '../StepTrigger.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' export type InterceptorOptions = { @@ -40,36 +41,10 @@ export namespace Interceptor { > = $Steps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] ? & { - [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> + [_ in $NextStep['name']]: StepTrigger.Infer<$NextStep, $NextNextSteps, $PipelineOutput> } & InferConstructorKeywordArguments_<$NextNextSteps, $PipelineOutput> : {} - - // dprint-ignore - interface InferStepTrigger<$Step extends Step, $NextSteps extends Step[], $PipelineOutput> { - ( - params?: Simplify< - & { - input?: $Step['input'] - } - & ( - $Step['slots'] extends undefined - ? {} - : { using?: { - [$SlotName in keyof $Step['slots']]?: Func.AppendAwaitedReturnType<$Step['slots'][$SlotName], undefined> - } - } - ) - > - ): Promise< - $NextSteps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] - ? { - [_ in $NextStep['name']]: InferStepTrigger<$NextStep, $NextNextSteps, $PipelineOutput> - } - : $PipelineOutput - > - input: $Step['input'] - } } export type InterceptorGeneric = NonRetryingInterceptor | RetryingInterceptor diff --git a/src/lib/anyware/Pipeline/FromSteps.ts b/src/lib/anyware/Pipeline/FromSteps.ts new file mode 100644 index 000000000..38c39cbba --- /dev/null +++ b/src/lib/anyware/Pipeline/FromSteps.ts @@ -0,0 +1,4 @@ +import type { Step } from '../Step.js' + +// todo +export type FromSteps<$StepDefinitions extends Step.Definition[]> = {} diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index 07fa5d5d4..bc88c0318 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,2 +1,4 @@ export * from './builder.js' +export * from './createFromType.js' +export * from './FromSteps.js' export * from './run.js' diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 7af002bfe..c2bc19e31 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -77,7 +77,7 @@ export interface Builder<$Context extends Context = Context> { slots: $Slots previous: GetNextStepParameterPrevious<$Context> }) => any, - $Slots extends undefined | Record = undefined, + $Slots extends undefined | Step.Slots = undefined, $Params extends { input: GetNextStepParameterInput<$Context> slots: $Slots @@ -88,7 +88,7 @@ export interface Builder<$Context extends Context = Context> { previous: GetNextStepParameterPrevious<$Context> }, >( - stepInput: { + parameters: { name: $Name slots?: $Slots run: $Run diff --git a/src/lib/anyware/Pipeline/createFromType.ts b/src/lib/anyware/Pipeline/createFromType.ts new file mode 100644 index 000000000..fde318fa7 --- /dev/null +++ b/src/lib/anyware/Pipeline/createFromType.ts @@ -0,0 +1,5 @@ +import type { Pipeline } from './__.js' + +export const createFromType = <$Pipeline extends Pipeline>(): $Pipeline => { + return undefined as any +} diff --git a/src/lib/anyware/Step.ts b/src/lib/anyware/Step.ts index 010db5147..f7aa8180a 100644 --- a/src/lib/anyware/Step.ts +++ b/src/lib/anyware/Step.ts @@ -4,13 +4,52 @@ export interface Step< $Name extends string = string, > { name: $Name - slots: Step.Slots + slots?: Step.Slots input: any output: any run: (params: any) => any } export namespace Step { + export type Definition = { + name: string + slots?: Step.Slots + input?: Input + output?: any + } + + /** + * todo + */ + export const createWithInput = < + $Input extends Input = Input, + >() => + < + const $Name extends string, + $Run extends ImplementationFn<$Input>, + $Slots extends undefined | Step.Slots, + >( + parameters: { + name: $Name + slots?: $Slots + run: $Run + }, + ): { + name: $Name + run: $Run + input: Parameters<$Run>[0]['input'] + output: ReturnType<$Run> + slots: undefined extends $Slots ? undefined : $Slots + } => { + // todo + parameters + return undefined as any + } + + type ImplementationFn<$Input extends Input = Input> = (parameters: { input: $Input }) => any + + export type Input = object + export type Slots = Record export type Name = string diff --git a/src/lib/anyware/StepTrigger.ts b/src/lib/anyware/StepTrigger.ts index 547eb575f..dffd5624f 100644 --- a/src/lib/anyware/StepTrigger.ts +++ b/src/lib/anyware/StepTrigger.ts @@ -1,9 +1,9 @@ -const stepTriggerSymbol = Symbol(`hook`) - -type StepTriggerSymbol = typeof stepTriggerSymbol +import type { Simplify } from 'type-fest' +import type { Func } from '../prelude.js' +import type { Step } from './Step.js' export namespace StepTrigger { - export const create = <$OriginalInput, $Fn extends StepTriggerBase>( + export const create = <$OriginalInput, $Fn extends StepTriggerRaw>( originalInput: $OriginalInput, fn: $Fn, ): StepTrigger<$Fn> => { @@ -13,16 +13,54 @@ export namespace StepTrigger { // @ts-expect-error return fn } -} -export interface StepTrigger< - $OriginalInput extends object = object, // Exclude[0], undefined>['input'], -> extends StepTriggerBase { - [stepTriggerSymbol]: StepTriggerSymbol - // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. - // E.g. adding `| unknown` would destroy the knowledge of hook envelope case - // todo this is not strictly true, it could also be the final result - input: $OriginalInput + export interface Properties< + $OriginalInput extends Step.Input = Step.Input, + > { + [stepTriggerSymbol]: StepTriggerSymbol + // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. + // E.g. adding `| unknown` would destroy the knowledge of hook envelope case + // todo this is not strictly true, it could also be the final result + input: $OriginalInput + } + + // dprint-ignore + export interface Infer< + $Step extends Step, + $NextSteps extends Step[], + $PipelineOutput> extends StepTrigger.Properties<$Step['input'] + > { + ( + params?: Simplify< + & { + input?: $Step['input'] + } + & ( + $Step['slots'] extends undefined + ? {} + : { using?: { + [$SlotName in keyof $Step['slots']]?: Func.AppendAwaitedReturnType<$Step['slots'][$SlotName], undefined> + } + } + ) + > + ): Promise< + $NextSteps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] + ? { + [_ in $NextStep['name']]: Infer<$NextStep, $NextNextSteps, $PipelineOutput> + } + : $PipelineOutput + > + } } -type StepTriggerBase = (input?: { input?: any; using?: any }) => any +export type StepTrigger< + $Fn extends StepTriggerRaw = StepTriggerRaw, + $OriginalInput extends Step.Input = Step.Input, +> = $Fn & StepTrigger.Properties<$OriginalInput> + +type StepTriggerRaw = (input?: { input?: any; using?: any }) => any + +const stepTriggerSymbol = Symbol(`hook`) + +type StepTriggerSymbol = typeof stepTriggerSymbol diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index 6ac01f25e..0984730bb 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -26,8 +26,8 @@ type PrivateHookRunnerInput = { previous: object } -export const createPipeline = () => { - return Pipeline.create() +export const createPipeline = (options?: Pipeline.Options) => { + return Pipeline.create(options) .step({ name: `a`, slots: { @@ -62,25 +62,29 @@ export const createPipeline = () => { type TestBuilder = ReturnType -// @ts-expect-error -export let builder: TestBuilder = null - // @ts-expect-error export let stepsIndex: Tuple.ToIndexByObjectKey = null beforeEach(() => { + const builder = createPipeline() stepsIndex = keyBy(builder.context.steps, _ => _.name) as any - builder = createPipeline() }) -export const runWithOptions = () => async (...interceptors: InterceptorInput[]) => { - return await Pipeline.run(builder, { - initialInput, - // @ts-expect-error fixme - interceptors, - }) +export const runWithOptions = (options?: Pipeline.Options) => { + const builder = createPipeline(options) + const run = async (...interceptors: InterceptorInput[]) => { + return await Pipeline.run(builder, { + initialInput, + // @ts-expect-error fixme + interceptors, + }) + } + return { + builder, + run, + } } -export const run = async (...extensions: InterceptorInput[]) => runWithOptions()(...extensions) +export const run = async (...extensions: InterceptorInput[]) => runWithOptions().run(...extensions) export const oops = new Error(`oops`) diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts index c5779dff6..bf2f8f169 100644 --- a/src/lib/anyware/__.test.ts +++ b/src/lib/anyware/__.test.ts @@ -6,6 +6,7 @@ import type { ContextualError } from '../errors/ContextualError.js' import { Pipeline } from './_.js' import { initialInput, oops, run, runWithOptions, stepsIndex } from './__.test-helpers.js' import { createRetryingInterceptor } from './Interceptor/Interceptor.js' +import { Step } from './Step.js' describe(`no extensions`, () => { test(`passthrough to implementation`, async () => { @@ -95,7 +96,7 @@ describe(`one extension`, () => { }) describe(`two extensions`, () => { - const run = runWithOptions({ entrypointSelectionMode: `optional` }) + const { run } = runWithOptions({ entrypointSelectionMode: `optional` }) test(`first can short-circuit`, async () => { const ex1 = () => 1 const ex2 = vi.fn().mockImplementation(() => 2) @@ -244,64 +245,49 @@ describe(`errors`, () => { } `) }) - // describe.skip('certain errors can be configured to be re-thrown without wrapping error', () => { - // class SpecialError1 extends Error {} - // class SpecialError2 extends Error {} - // // const a = createHook({ - // // slots: {}, - // // run: ({ input }: { slots: object; input: { throws: Error } }) => { - // // if (input.throws) throw input.throws - // // }, - // // }) + describe.skip('certain errors can be configured to be re-thrown without wrapping error', () => { + class SpecialError1 extends Error {} + class SpecialError2 extends Error {} + const stepA = Step.createWithInput<{ throws: Error }>()({ + name: 'a', + run: ({ input }) => { + if (input.throws) throw input.throws + }, + }) - // test('via passthroughErrorInstanceOf (one)', async () => { - // const builder = Pipeline.create<{ throws: Error }>({ - // passthroughErrorInstanceOf: [SpecialError1], - // }).step({ - // name: 'a', - // run: ({ input }) => { - // if (input.throws) throw input.throws - // }, - // }) + test('via passthroughErrorInstanceOf (one)', async () => { + const builder = Pipeline.create<{ throws: Error }>({ + passthroughErrorInstanceOf: [SpecialError1], + }).step(stepA) - // // dprint-ignore - // expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // // dprint-ignore - // expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) - // }) - // test('via passthroughErrorInstanceOf (multiple)', async () => { - // const builder = Pipeline.create<{ throws: Error }>({ - // passthroughErrorInstanceOf: [SpecialError1, SpecialError2], - // }).step({ - // name: 'a', - // run: ({ input }) => { - // if (input.throws) throw input.throws - // }, - // }) - // // dprint-ignore - // expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // // dprint-ignore - // expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) - // }) - // test('via passthroughWith', async () => { - // const builder = Pipeline.create<{ throws: Error }>({ - // // todo type-safe hook name according to values passed to constructor - // // todo type-tests on signal { hookName, source, error } - // passthroughErrorWith: (signal) => { - // return signal.error instanceof SpecialError1 - // }, - // }).step({ - // name: 'a', - // run: ({ input }) => { - // if (input.throws) throw input.throws - // }, - // }) - // // dprint-ignore - // expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // // dprint-ignore - // expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) - // }) - // }) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + }) + test('via passthroughErrorInstanceOf (multiple)', async () => { + const builder = Pipeline.create<{ throws: Error }>({ + passthroughErrorInstanceOf: [SpecialError1, SpecialError2], + }).step(stepA) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) + }) + test('via passthroughWith', async () => { + const builder = Pipeline.create<{ throws: Error }>({ + // todo type-safe hook name according to values passed to constructor + // todo type-tests on signal { hookName, source, error } + passthroughErrorWith: (signal) => { + return signal.error instanceof SpecialError1 + }, + }).step(stepA) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + }) + }) }) describe('retrying extension', () => { diff --git a/src/lib/prelude.test-d.ts b/src/lib/prelude.test-d.ts index 59acee09f..8b44f0b32 100644 --- a/src/lib/prelude.test-d.ts +++ b/src/lib/prelude.test-d.ts @@ -1,5 +1,5 @@ import { assertEqual } from './assert-equal.js' -import { type GetLastValue, type OmitKeysWithPrefix, type ToParameters, type Tuple } from './prelude.js' +import { type OmitKeysWithPrefix, type ToParameters, type Tuple } from './prelude.js' // dprint-ignore { @@ -13,12 +13,12 @@ assertEqual , []>() assertEqual , [{ a:1; b?:2 }]>() assertEqual , [{ a?:1; b?:2 }]|[]>() -assertEqual, 3>() +// Tuple.* + +assertEqual, 3>() // @ts-expect-error GetLastValue<[]> -// Tuple.* - assertEqual, [1, 2, 3]>() assertEqual, [3]>() assertEqual, []>() diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 0b7738bc2..6537f0723 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -254,9 +254,9 @@ export type MaybePromise = T | Promise export const capitalizeFirstLetter = (string: string) => string.charAt(0).toUpperCase() + string.slice(1) -export type SomeAsyncFunction = (...args: unknown[]) => Promise +export type SomeAsyncFunction = (...args: any[]) => Promise -export type SomeFunction = (...args: unknown[]) => MaybePromise +export type SomeFunction = (...args: any[]) => MaybePromise export type Deferred = { promise: Promise diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index aa695bc21..a195bdd2f 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -12,7 +12,7 @@ import { } from '../lib/grafaid/http/http.js' import { normalizeRequestToNode } from '../lib/grafaid/request.js' import { mergeRequestInit, searchParamsAppendAll } from '../lib/http.js' -import { casesExhausted, isString } from '../lib/prelude.js' +import { casesExhausted, isString, type MaybePromise } from '../lib/prelude.js' import { Transport } from '../types/Transport.js' import { decodeResultData } from './CustomScalars/decode.js' import { encodeRequestVariables } from './CustomScalars/encode.js' @@ -24,186 +24,193 @@ import type { MethodModePost } from '../client/transportHttp/request.js' import type { httpMethodGet, httpMethodPost } from '../lib/http.js' import type { TransportHttp, TransportMemory } from '../types/Transport.js' -export type RequestPipeline = Anyware.Pipeline.Infer +export type RequestPipeline<$Config extends Config = Config> = Anyware.Pipeline.FromSteps<[ + RequestPipeline.Steps.HookDefEncode<$Config>, + RequestPipeline.Steps.HookDefPack<$Config>, + RequestPipeline.Steps.HookDefExchange<$Config>, + RequestPipeline.Steps.HookDefUnpack<$Config>, + RequestPipeline.Steps.HookDefDecode<$Config>, +]> export const RequestPipeline = Anyware.Pipeline - .create() - .step({ - name: `encode`, - run: ({ input }): RequestPipeline.Hooks.HookDefPack['input'] => { - const sddm = input.state.schemaMap - const scalars = input.state.scalars.map - if (sddm) { - const request = normalizeRequestToNode(input.request) + .createFromType({ + steps: [], // todo + }) +// .create() +// .step({ +// name: `encode`, +// run: ({ input }): RequestPipeline.Steps.HookDefPack['input'] => { +// const sddm = input.state.schemaMap +// const scalars = input.state.scalars.map +// if (sddm) { +// const request = normalizeRequestToNode(input.request) - // We will mutate query. Assign it back to input for it to be carried forward. - input.request.query = request.query +// // We will mutate query. Assign it back to input for it to be carried forward. +// input.request.query = request.query - encodeRequestVariables({ sddm, scalars, request }) - } +// encodeRequestVariables({ sddm, scalars, request }) +// } - return input - }, - }) - .step({ - name: `pack`, - slots: { - searchParams: getRequestEncodeSearchParameters, - body: postRequestEncodeBody, - }, - run: ({ input, slots }) => { - const graphqlRequest: Grafaid.HTTP.RequestConfig = { - operationName: input.request.operationName, - variables: input.request.variables, - query: print(input.request.query), - } +// return input +// }, +// }) +// .step({ +// name: `pack`, +// slots: { +// searchParams: getRequestEncodeSearchParameters, +// body: postRequestEncodeBody, +// }, +// run: ({ input, slots }) => { +// const graphqlRequest: Grafaid.HTTP.RequestConfig = { +// operationName: input.request.operationName, +// variables: input.request.variables, +// query: print(input.request.query), +// } - // TODO thrown error here is swallowed in examples. - switch (input.transportType) { - case `memory`: { - return { - ...input, - request: graphqlRequest, - } - } - case `http`: { - if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) +// // TODO thrown error here is swallowed in examples. +// switch (input.transportType) { +// case `memory`: { +// return { +// ...input, +// request: graphqlRequest, +// } +// } +// case `http`: { +// if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) - const operationType = isString(input.request.operation) - ? input.request.operation - : input.request.operation.operation - const methodMode = input.state.config.transport.config.methodMode - const requestMethod = methodMode === MethodMode.post - ? `post` - : methodMode === MethodMode.getReads - ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` - : casesExhausted(methodMode) +// const operationType = isString(input.request.operation) +// ? input.request.operation +// : input.request.operation.operation +// const methodMode = input.state.config.transport.config.methodMode +// const requestMethod = methodMode === MethodMode.post +// ? `post` +// : methodMode === MethodMode.getReads +// ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` +// : casesExhausted(methodMode) - const baseProperties = mergeRequestInit( - mergeRequestInit( - mergeRequestInit( - { - headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec, - }, - { - headers: input.state.config.transport.config.headers, - }, - ), - input.state.config.transport.config.raw, - ), - { - headers: input.headers, - }, - ) - const request: RequestPipeline.Hooks.CoreExchangePostRequest | RequestPipeline.Hooks.CoreExchangeGetRequest = - requestMethod === `get` - ? { - methodMode: methodMode as MethodModeGetReads, - ...baseProperties, - method: `get`, - url: searchParamsAppendAll(input.url, slots.searchParams(graphqlRequest)), - } - : { - methodMode: methodMode, - ...baseProperties, - method: `post`, - url: input.url, - body: slots.body(graphqlRequest), - } - return { - ...input, - request, - } - } - default: - throw casesExhausted(input) - } - }, - }) - .step({ - name: `exchange`, - slots: { - // Put fetch behind a lambda so that it can be easily globally overridden - // by fixtures. - fetch: (requestInfo: RequestInfo) => fetch(requestInfo), - }, - run: async ({ input, slots }) => { - switch (input.transportType) { - case `http`: { - const request = new Request(input.request.url, input.request) - const response = await slots.fetch(request) - return { - ...input, - response, - } - } - case `memory`: { - const result = await execute(input) - return { - ...input, - result, - } - } - default: - throw casesExhausted(input) - } - }, - }) - .step({ - name: `unpack`, - run: async ({ input }) => { - switch (input.transportType) { - case `http`: { - // todo 1 if response is missing header of content length then .json() hangs forever. - // firstly consider a timeout, secondly, if response is malformed, then don't even run .json() - // todo 2 if response is e.g. 404 with no json body, then an error is thrown because json parse cannot work, not gracefully handled here - const json = await input.response.json() as object - const result = parseExecutionResult(json) - return { - ...input, - result, - } - } - case `memory`: { - return { - ...input, - result: input.result, - } - } - default: - throw casesExhausted(input) - } - }, - }) - .step({ - name: `decode`, - run: ({ input, previous }) => { - // If there has been an error and we definitely don't have any data, such as when - // giving an operation name that doesn't match any in the document, - // then don't attempt to decode. - const isError = !input.result.data && (input.result.errors?.length ?? 0) > 0 - if (input.state.schemaMap && !isError) { - decodeResultData({ - sddm: input.state.schemaMap, - request: normalizeRequestToNode(previous.pack.input.request), - data: input.result.data, - scalars: input.state.scalars.map, - }) - } +// const baseProperties = mergeRequestInit( +// mergeRequestInit( +// mergeRequestInit( +// { +// headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec, +// }, +// { +// headers: input.state.config.transport.config.headers, +// }, +// ), +// input.state.config.transport.config.raw, +// ), +// { +// headers: input.headers, +// }, +// ) +// const request: RequestPipeline.Steps.CoreExchangePostRequest | RequestPipeline.Steps.CoreExchangeGetRequest = +// requestMethod === `get` +// ? { +// methodMode: methodMode as MethodModeGetReads, +// ...baseProperties, +// method: `get`, +// url: searchParamsAppendAll(input.url, slots.searchParams(graphqlRequest)), +// } +// : { +// methodMode: methodMode, +// ...baseProperties, +// method: `post`, +// url: input.url, +// body: slots.body(graphqlRequest), +// } +// return { +// ...input, +// request, +// } +// } +// default: +// throw casesExhausted(input) +// } +// }, +// }) +// .step({ +// name: `exchange`, +// slots: { +// fetch: (requestInfo: RequestInfo): MaybePromise => fetch(requestInfo), +// }, +// run: async ({ input, slots }) => { +// switch (input.transportType) { +// case `http`: { +// const request = new Request(input.request.url, input.request) +// const response = await slots.fetch(request) +// return { +// ...input, +// response, +// } +// } +// case `memory`: { +// const result = await execute(input) +// return { +// ...input, +// result, +// } +// } +// default: +// throw casesExhausted(input) +// } +// }, +// }) +// .step({ +// name: `unpack`, +// run: async ({ input }) => { +// switch (input.transportType) { +// case `http`: { +// // todo 1 if response is missing header of content length then .json() hangs forever. +// // firstly consider a timeout, secondly, if response is malformed, then don't even run .json() +// // todo 2 if response is e.g. 404 with no json body, then an error is thrown because json parse cannot work, not gracefully handled here +// const json = await input.response.json() as object +// const result = parseExecutionResult(json) +// return { +// ...input, +// result, +// } +// } +// case `memory`: { +// return { +// ...input, +// result: input.result, +// } +// } +// default: +// throw casesExhausted(input) +// } +// }, +// }) +// .step({ +// name: `decode`, +// run: ({ input, previous }) => { +// // If there has been an error and we definitely don't have any data, such as when +// // giving an operation name that doesn't match any in the document, +// // then don't attempt to decode. +// const isError = !input.result.data && (input.result.errors?.length ?? 0) > 0 +// if (input.state.schemaMap && !isError) { +// decodeResultData({ +// sddm: input.state.schemaMap, +// request: normalizeRequestToNode(previous.pack.input.request), +// data: input.result.data, +// scalars: input.state.scalars.map, +// }) +// } - const result = input.transportType === `http` - ? { - ...input.result, - response: input.response, - } - : input.result +// const result = input.transportType === `http` +// ? { +// ...input.result, +// response: input.response, +// } +// : input.result - return result - }, - }) +// return result +// }, +// }) export namespace RequestPipeline { - export namespace Hooks { + export namespace Steps { export interface HookInputBase { state: Context } @@ -231,6 +238,7 @@ export namespace RequestPipeline { // --------------------------- export type HookDefEncode<$Config extends Config = Config> = { + name: `encode` input: & { request: Grafaid.RequestAnalyzedInput } & HookInputBase @@ -238,6 +246,7 @@ export namespace RequestPipeline { } export type HookDefPack<$Config extends Config = Config> = { + name: `pack` input: & HookInputBase & TransportInput< @@ -259,6 +268,7 @@ export namespace RequestPipeline { } export type HookDefExchange<$Config extends Config> = { + name: `exchange` slots: { fetch: (request: Request) => Response | Promise } @@ -271,33 +281,27 @@ export namespace RequestPipeline { > } - // export type HookDefUnpack<$Config extends Config> = { - // input: - // & HookInputBase - // & TransportInput< - // $Config, - // { response: Response }, - // { result: FormattedExecutionResult } - // > - // } - - // export type HookDefDecode<$Config extends Config> = { - // input: - // & HookInputBase - // & TransportInput< - // $Config, - // { response: Response } - // > - // & { result: FormattedExecutionResult } - // } + export type HookDefUnpack<$Config extends Config> = { + name: `unpack` + input: + & HookInputBase + & TransportInput< + $Config, + { response: Response }, + { result: FormattedExecutionResult } + > + } - // export type HookMap<$Config extends Config = Config> = { - // encode: HookDefEncode<$Config> - // pack: HookDefPack<$Config> - // exchange: HookDefExchange<$Config> - // unpack: HookDefUnpack<$Config> - // decode: HookDefDecode<$Config> - // } + export type HookDefDecode<$Config extends Config> = { + name: `decode` + input: + & HookInputBase + & TransportInput< + $Config, + { response: Response } + > + & { result: FormattedExecutionResult } + } /** * An extension of {@link RequestInit} that adds a required `url` property and makes `body` required. diff --git a/tests/_/SpyExtension.ts b/tests/_/SpyExtension.ts index abdb26c01..eeb71db84 100644 --- a/tests/_/SpyExtension.ts +++ b/tests/_/SpyExtension.ts @@ -5,13 +5,13 @@ import type { RequestPipeline } from '../../src/requestPipeline/__.js' interface SpyData { encode: { - input: RequestPipeline.Hooks.HookDefEncode['input'] | null + input: RequestPipeline.Steps.HookDefEncode['input'] | null } pack: { - input: RequestPipeline.Hooks.HookDefPack['input'] | null + input: RequestPipeline.Steps.HookDefPack['input'] | null } exchange: { - input: RequestPipeline.Hooks.HookDefExchange['input'] | null + input: RequestPipeline.Steps.HookDefExchange['input'] | null } } diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts index 39192c82f..fd7f2396a 100644 --- a/tests/_/helpers.ts +++ b/tests/_/helpers.ts @@ -116,12 +116,10 @@ export const test = testBase.extend({ }, kitchenSink: async ({ fetch: _ }, use) => { const kitchenSink = KitchenSink.create({ schema: kitchenSinkSchema }) - // @ts-expect-error fixme await use(kitchenSink) }, kitchenSinkHttp: async ({ fetch: _ }, use) => { const kitchenSink = KitchenSink.create({ schema: `https://foo.io/api/graphql` }) - // @ts-expect-error fixme await use(kitchenSink) }, kitchenSinkData: async ({}, use) => { // eslint-disable-line From da94fff5e3588d2693920711b5a5ae7f1d6e6216 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 7 Nov 2024 23:46:12 -0500 Subject: [PATCH 17/36] wip --- src/client/builderExtensions/anyware.ts | 6 +- src/client/gql/gql.ts | 2 +- .../requestMethods/requestMethods.ts | 2 +- src/extension/extension.ts | 6 +- src/lib/anyware/Pipeline/Config.ts | 34 ++ src/lib/anyware/Pipeline/FromSteps.ts | 4 - src/lib/anyware/Pipeline/_.ts | 1 - src/lib/anyware/Pipeline/__.ts | 11 +- src/lib/anyware/Pipeline/builder.ts | 35 +- src/lib/anyware/Pipeline/createFromType.ts | 66 +++- src/lib/anyware/Pipeline/types.ts | 24 ++ src/lib/anyware/PipelineDefinition.ts | 5 + src/lib/anyware/Step.ts | 7 - src/lib/anyware/StepDefinition.ts | 8 + src/lib/anyware/_.ts | 1 + src/requestPipeline/RequestPipeline.ts | 354 +++++++++--------- tests/_/SpyExtension.ts | 2 +- 17 files changed, 327 insertions(+), 241 deletions(-) create mode 100644 src/lib/anyware/Pipeline/Config.ts delete mode 100644 src/lib/anyware/Pipeline/FromSteps.ts create mode 100644 src/lib/anyware/Pipeline/types.ts create mode 100644 src/lib/anyware/PipelineDefinition.ts create mode 100644 src/lib/anyware/StepDefinition.ts diff --git a/src/client/builderExtensions/anyware.ts b/src/client/builderExtensions/anyware.ts index d0d8ac4dc..d4d27c529 100644 --- a/src/client/builderExtensions/anyware.ts +++ b/src/client/builderExtensions/anyware.ts @@ -1,7 +1,7 @@ import { createExtension } from '../../extension/extension.js' import type { Anyware as AnywareLib } from '../../lib/anyware/__.js' import { Builder } from '../../lib/builder/__.js' -import type { RequestPipeline } from '../../requestPipeline/__.js' +import type { RequestPipelineDefinition } from '../../requestPipeline/__.js' import { type Context } from '../context.js' export interface BuilderExtensionAnyware extends Builder.Extension { @@ -16,14 +16,14 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters + RequestPipelineDefinition // <$Arguments['context']['config']> >, ) => Builder.Definition.MaterializeWithNewContext<$Arguments['chain'], $Arguments['context']> } export const builderExtensionAnyware = Builder.Extension.create((builder, context) => { const properties = { - anyware: (interceptor: AnywareLib.Interceptor.InferConstructor) => { + anyware: (interceptor: AnywareLib.Interceptor.InferConstructor) => { return builder({ ...context, extensions: [ diff --git a/src/client/gql/gql.ts b/src/client/gql/gql.ts index 4c3e4cdf5..c1f68536c 100644 --- a/src/client/gql/gql.ts +++ b/src/client/gql/gql.ts @@ -7,7 +7,7 @@ import { joinTemplateStringArrayAndArgs, type TemplateStringsArguments, } from '../../lib/template-string.js' -import { RequestPipeline } from '../../requestPipeline/__.js' // todo +import { RequestPipelineDefinition } from '../../requestPipeline/__.js' // todo import { type Context } from '../context.js' import { handleOutput } from '../handleOutput.js' import type { Config } from '../Settings/Config.js' diff --git a/src/documentBuilder/requestMethods/requestMethods.ts b/src/documentBuilder/requestMethods/requestMethods.ts index 5b4d76904..6a080dfaa 100644 --- a/src/documentBuilder/requestMethods/requestMethods.ts +++ b/src/documentBuilder/requestMethods/requestMethods.ts @@ -9,7 +9,7 @@ import { Builder } from '../../lib/builder/__.js' import type { Grafaid } from '../../lib/grafaid/__.js' import { getOperationDefinition } from '../../lib/grafaid/document.js' import { isSymbol } from '../../lib/prelude.js' -import { RequestPipeline } from '../../requestPipeline/__.js' +import { RequestPipelineDefinition } from '../../requestPipeline/__.js' import type { GlobalRegistry } from '../../types/GlobalRegistry/GlobalRegistry.js' import { Select } from '../Select/__.js' import { SelectionSetGraphqlMapper } from '../SelectGraphQLMapper/__.js' diff --git a/src/extension/extension.ts b/src/extension/extension.ts index cfc81f1ed..286855e72 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -8,7 +8,7 @@ import type { Builder } from '../lib/builder/__.js' import type { AssertExtends } from '../lib/prelude.js' import type { TypeFunction } from '../lib/type-function/__.js' import type { Fn } from '../lib/type-function/TypeFunction.js' -import type { RequestPipeline } from '../requestPipeline/__.js' +import type { RequestPipelineDefinition } from '../requestPipeline/__.js' import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js' export interface TypeHooks { @@ -57,7 +57,7 @@ export interface Extension< /** * Anyware executed on every request. */ - onRequest?: Anyware.Interceptor.InferConstructor + onRequest?: Anyware.Interceptor.InferConstructor /** * Manipulate the builder. * You can extend the builder with new properties at both runtime AND buildtime (types, TypeScript). @@ -169,7 +169,7 @@ export const createExtension = < custom?: $Custom create: (params: { config: $Config }) => { builder?: $BuilderExtension - onRequest?: Anyware.Interceptor.InferConstructor + onRequest?: Anyware.Interceptor.InferConstructor typeHooks?: () => $TypeHooks } }, diff --git a/src/lib/anyware/Pipeline/Config.ts b/src/lib/anyware/Pipeline/Config.ts new file mode 100644 index 000000000..1cc9f1884 --- /dev/null +++ b/src/lib/anyware/Pipeline/Config.ts @@ -0,0 +1,34 @@ +import type { StepResultError } from '../StepResult.js' + +export interface Options { + /** + * @defaultValue `required` + */ + entrypointSelectionMode?: 'optional' | 'required' | 'off' + /** + * If a hook results in a thrown error but is an instance of one of these classes then return it as-is + * rather than wrapping it in a ContextualError. + * + * This can be useful when there are known kinds of errors such as Abort Errors from AbortController + * which are actually a signaling mechanism. + */ + passthroughErrorInstanceOf?: Function[] + /** + * If a hook results in a thrown error but returns true from this function then return the error as-is + * rather than wrapping it in a ContextualError. + * + * This can be useful when there are known kinds of errors such as Abort Errors from AbortController + * which are actually a signaling mechanism. + */ + passthroughErrorWith?: null | ((signal: StepResultError) => boolean) +} + +export type Config = Required + +export const resolveOptions = (options?: Options): Config => { + return { + passthroughErrorInstanceOf: options?.passthroughErrorInstanceOf ?? [], + passthroughErrorWith: options?.passthroughErrorWith ?? null, + entrypointSelectionMode: options?.entrypointSelectionMode ?? `required`, + } +} diff --git a/src/lib/anyware/Pipeline/FromSteps.ts b/src/lib/anyware/Pipeline/FromSteps.ts deleted file mode 100644 index 38c39cbba..000000000 --- a/src/lib/anyware/Pipeline/FromSteps.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { Step } from '../Step.js' - -// todo -export type FromSteps<$StepDefinitions extends Step.Definition[]> = {} diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index bc88c0318..217c9e1f8 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,4 +1,3 @@ export * from './builder.js' export * from './createFromType.js' -export * from './FromSteps.js' export * from './run.js' diff --git a/src/lib/anyware/Pipeline/__.ts b/src/lib/anyware/Pipeline/__.ts index f14d876b6..b8a07a76e 100644 --- a/src/lib/anyware/Pipeline/__.ts +++ b/src/lib/anyware/Pipeline/__.ts @@ -1,5 +1,12 @@ -import type { Context } from './builder.js' +import type { Step } from '../Step.js' +import type { Config } from './Config.js' export * as Pipeline from './_.js' -export type Pipeline = Context +export interface Pipeline { + input: object + // todo + // output: object + steps: Step[] + config: Config +} diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index c2bc19e31..15983635e 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,8 +1,8 @@ import type { ConfigManager } from '../../config-manager/__.js' import { type Tuple } from '../../prelude.js' import type { Step } from '../Step.js' -import type { StepResultError } from '../StepResult.js' import type { Pipeline } from './__.js' +import { type Config, type Options, resolveOptions } from './Config.js' export interface Context { input: object @@ -113,31 +113,6 @@ export interface Builder<$Context extends Context = Context> { export type Infer<$Builder extends Builder> = $Builder['context'] -export interface Options { - /** - * @defaultValue `required` - */ - entrypointSelectionMode?: 'optional' | 'required' | 'off' - /** - * If a hook results in a thrown error but is an instance of one of these classes then return it as-is - * rather than wrapping it in a ContextualError. - * - * This can be useful when there are known kinds of errors such as Abort Errors from AbortController - * which are actually a signaling mechanism. - */ - passthroughErrorInstanceOf?: Function[] - /** - * If a hook results in a thrown error but returns true from this function then return the error as-is - * rather than wrapping it in a ContextualError. - * - * This can be useful when there are known kinds of errors such as Abort Errors from AbortController - * which are actually a signaling mechanism. - */ - passthroughErrorWith?: null | ((signal: StepResultError) => boolean) -} - -export type Config = Required - /** * TODO */ @@ -149,11 +124,3 @@ export const create = <$Input extends object>(options?: Options): Builder<{ const _config = resolveOptions(options) return undefined as any } - -const resolveOptions = (options?: Options): Config => { - return { - passthroughErrorInstanceOf: options?.passthroughErrorInstanceOf ?? [], - passthroughErrorWith: options?.passthroughErrorWith ?? null, - entrypointSelectionMode: options?.entrypointSelectionMode ?? `required`, - } -} diff --git a/src/lib/anyware/Pipeline/createFromType.ts b/src/lib/anyware/Pipeline/createFromType.ts index fde318fa7..19fd0daff 100644 --- a/src/lib/anyware/Pipeline/createFromType.ts +++ b/src/lib/anyware/Pipeline/createFromType.ts @@ -1,5 +1,67 @@ -import type { Pipeline } from './__.js' +import type { ConfigManager } from '../../config-manager/__.js' +import type { Tuple } from '../../prelude.js' +import type { PipelineDefinition } from '../PipelineDefinition.js' +import type { Step } from '../Step.js' +import type { StepDefinition } from '../StepDefinition.js' +import { type Config, type Options, resolveOptions } from './Config.js' -export const createFromType = <$Pipeline extends Pipeline>(): $Pipeline => { +// dprint-ignore +type InferInputSteps<$PreviousStepDefinitions extends StepDefinition[], $NextStepDefinitions extends StepDefinition[]> = + $NextStepDefinitions extends [infer $StepDefinition extends StepDefinition, ...infer $RestStepDefinitions extends StepDefinition[]] + ? [ + & { + name: $StepDefinition['name'] + run: (parameters: + & { + input: $StepDefinition['input'] + previous: GetParameterPrevious<$PreviousStepDefinitions> + } + & ( + $StepDefinition['slots'] extends Step.Slots + ? { + slots: $StepDefinition['slots'] + } + : {} + ) + ) => $StepDefinition['output'] + } + & ( + $StepDefinition['slots'] extends Step.Slots + ? { + slots: $StepDefinition['slots'] + } + : {} + ) + , ...InferInputSteps<[...$PreviousStepDefinitions, $StepDefinition], $RestStepDefinitions>] + : [] + +export type GetParameterPrevious<$StepDefinitions extends StepDefinition[]> = Tuple.IntersectItems< + { + [$Index in keyof $StepDefinitions]: { + [$StepName in $StepDefinitions[$Index]['name']]: { + input: $StepDefinitions[$Index]['input'] + output: ConfigManager.OrDefault< + $StepDefinitions[$Index]['output'], + Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] + > + } + } + } +> + +type InferPipeline<$PipelineDefinition extends PipelineDefinition> = { + input: $PipelineDefinition['stepDefinitions'][0]['input'] + steps: InferInputSteps<[], $PipelineDefinition['stepDefinitions']> + config: Config +} + +export const createWithType = <$PipelineDefinition extends PipelineDefinition>( + input: { + options?: Options + steps: InferInputSteps<[], $PipelineDefinition['stepDefinitions']> + }, +): InferPipeline<$PipelineDefinition> => { + const _config = resolveOptions(input.options) + input return undefined as any } diff --git a/src/lib/anyware/Pipeline/types.ts b/src/lib/anyware/Pipeline/types.ts new file mode 100644 index 000000000..ae3fa26b9 --- /dev/null +++ b/src/lib/anyware/Pipeline/types.ts @@ -0,0 +1,24 @@ +import type { ConfigManager } from '../../config-manager/__.js' +import type { Tuple } from '../../prelude.js' +import type { StepDefinition } from '../StepDefinition.js' +import type { Pipeline } from './__.js' + +// dprint-ignore +export type GetNextStepParameterPrevious<$Pipeline extends Pipeline> = + $Pipeline['steps'] extends [any, ...any[]] + ? GetNextStepPrevious_<$Pipeline['steps']> + : undefined + +export type GetNextStepPrevious_<$StepDefinitions extends StepDefinition[]> = Tuple.IntersectItems< + { + [$Index in keyof $StepDefinitions]: { + [$StepName in $StepDefinitions[$Index]['name']]: { + input: $StepDefinitions[$Index]['input'] + output: ConfigManager.OrDefault< + $StepDefinitions[$Index]['output'], + Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] + > + } + } + } +> diff --git a/src/lib/anyware/PipelineDefinition.ts b/src/lib/anyware/PipelineDefinition.ts new file mode 100644 index 000000000..bb67d4e87 --- /dev/null +++ b/src/lib/anyware/PipelineDefinition.ts @@ -0,0 +1,5 @@ +import type { StepDefinition } from './StepDefinition.js' + +export interface PipelineDefinition<$StepDefinitions extends StepDefinition[] = StepDefinition[]> { + stepDefinitions: $StepDefinitions +} diff --git a/src/lib/anyware/Step.ts b/src/lib/anyware/Step.ts index f7aa8180a..d3ef16de2 100644 --- a/src/lib/anyware/Step.ts +++ b/src/lib/anyware/Step.ts @@ -11,13 +11,6 @@ export interface Step< } export namespace Step { - export type Definition = { - name: string - slots?: Step.Slots - input?: Input - output?: any - } - /** * todo */ diff --git a/src/lib/anyware/StepDefinition.ts b/src/lib/anyware/StepDefinition.ts new file mode 100644 index 000000000..f5f4bc55b --- /dev/null +++ b/src/lib/anyware/StepDefinition.ts @@ -0,0 +1,8 @@ +import type { Step } from './Step.js' + +export type StepDefinition = { + name: string + slots?: Step.Slots + input?: Step.Input + output?: any +} diff --git a/src/lib/anyware/_.ts b/src/lib/anyware/_.ts index bc46832d5..131505d9f 100644 --- a/src/lib/anyware/_.ts +++ b/src/lib/anyware/_.ts @@ -1,3 +1,4 @@ export * from './Interceptor/Interceptor.js' export * from './Pipeline/__.js' +export * from './PipelineDefinition.js' export * from './run/runner.js' diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index a195bdd2f..aaaa0dfc7 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -3,13 +3,8 @@ import { Anyware } from '../lib/anyware/__.js' import type { Grafaid } from '../lib/grafaid/__.js' import { OperationTypeToAccessKind, print } from '../lib/grafaid/document.js' import { execute } from '../lib/grafaid/execute.js' // todo -import { - getRequestEncodeSearchParameters, - getRequestHeadersRec, - parseExecutionResult, - postRequestEncodeBody, - postRequestHeadersRec, -} from '../lib/grafaid/http/http.js' +import { getRequestEncodeSearchParameters, postRequestEncodeBody } from '../lib/grafaid/http/http.js' +import { getRequestHeadersRec, parseExecutionResult, postRequestHeadersRec } from '../lib/grafaid/http/http.js' import { normalizeRequestToNode } from '../lib/grafaid/request.js' import { mergeRequestInit, searchParamsAppendAll } from '../lib/http.js' import { casesExhausted, isString, type MaybePromise } from '../lib/prelude.js' @@ -24,192 +19,187 @@ import type { MethodModePost } from '../client/transportHttp/request.js' import type { httpMethodGet, httpMethodPost } from '../lib/http.js' import type { TransportHttp, TransportMemory } from '../types/Transport.js' -export type RequestPipeline<$Config extends Config = Config> = Anyware.Pipeline.FromSteps<[ - RequestPipeline.Steps.HookDefEncode<$Config>, - RequestPipeline.Steps.HookDefPack<$Config>, - RequestPipeline.Steps.HookDefExchange<$Config>, - RequestPipeline.Steps.HookDefUnpack<$Config>, - RequestPipeline.Steps.HookDefDecode<$Config>, -]> - export const RequestPipeline = Anyware.Pipeline - .createFromType({ - steps: [], // todo - }) -// .create() -// .step({ -// name: `encode`, -// run: ({ input }): RequestPipeline.Steps.HookDefPack['input'] => { -// const sddm = input.state.schemaMap -// const scalars = input.state.scalars.map -// if (sddm) { -// const request = normalizeRequestToNode(input.request) + .createWithType({ + steps: [{ + name: `encode`, + run: ({ input }): RequestPipeline.Steps.HookDefPack['input'] => { + const sddm = input.state.schemaMap + const scalars = input.state.scalars.map + if (sddm) { + const request = normalizeRequestToNode(input.request) -// // We will mutate query. Assign it back to input for it to be carried forward. -// input.request.query = request.query + // We will mutate query. Assign it back to input for it to be carried forward. + input.request.query = request.query -// encodeRequestVariables({ sddm, scalars, request }) -// } + encodeRequestVariables({ sddm, scalars, request }) + } -// return input -// }, -// }) -// .step({ -// name: `pack`, -// slots: { -// searchParams: getRequestEncodeSearchParameters, -// body: postRequestEncodeBody, -// }, -// run: ({ input, slots }) => { -// const graphqlRequest: Grafaid.HTTP.RequestConfig = { -// operationName: input.request.operationName, -// variables: input.request.variables, -// query: print(input.request.query), -// } + return input + }, + }, { + name: `pack`, + slots: { + searchParams: getRequestEncodeSearchParameters, + body: postRequestEncodeBody, + }, + run: ({ input, slots }) => { + const graphqlRequest: Grafaid.HTTP.RequestConfig = { + operationName: input.request.operationName, + variables: input.request.variables, + query: print(input.request.query), + } -// // TODO thrown error here is swallowed in examples. -// switch (input.transportType) { -// case `memory`: { -// return { -// ...input, -// request: graphqlRequest, -// } -// } -// case `http`: { -// if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) + // TODO thrown error here is swallowed in examples. + switch (input.transportType) { + case `memory`: { + return { + ...input, + request: graphqlRequest, + } + } + case `http`: { + if (input.state.config.transport.type !== Transport.http) throw new Error(`transport type is not http`) -// const operationType = isString(input.request.operation) -// ? input.request.operation -// : input.request.operation.operation -// const methodMode = input.state.config.transport.config.methodMode -// const requestMethod = methodMode === MethodMode.post -// ? `post` -// : methodMode === MethodMode.getReads -// ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` -// : casesExhausted(methodMode) + const operationType = isString(input.request.operation) + ? input.request.operation + : input.request.operation.operation + const methodMode = input.state.config.transport.config.methodMode + const requestMethod = methodMode === MethodMode.post + ? `post` + : methodMode === MethodMode.getReads + ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` + : casesExhausted(methodMode) -// const baseProperties = mergeRequestInit( -// mergeRequestInit( -// mergeRequestInit( -// { -// headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec, -// }, -// { -// headers: input.state.config.transport.config.headers, -// }, -// ), -// input.state.config.transport.config.raw, -// ), -// { -// headers: input.headers, -// }, -// ) -// const request: RequestPipeline.Steps.CoreExchangePostRequest | RequestPipeline.Steps.CoreExchangeGetRequest = -// requestMethod === `get` -// ? { -// methodMode: methodMode as MethodModeGetReads, -// ...baseProperties, -// method: `get`, -// url: searchParamsAppendAll(input.url, slots.searchParams(graphqlRequest)), -// } -// : { -// methodMode: methodMode, -// ...baseProperties, -// method: `post`, -// url: input.url, -// body: slots.body(graphqlRequest), -// } -// return { -// ...input, -// request, -// } -// } -// default: -// throw casesExhausted(input) -// } -// }, -// }) -// .step({ -// name: `exchange`, -// slots: { -// fetch: (requestInfo: RequestInfo): MaybePromise => fetch(requestInfo), -// }, -// run: async ({ input, slots }) => { -// switch (input.transportType) { -// case `http`: { -// const request = new Request(input.request.url, input.request) -// const response = await slots.fetch(request) -// return { -// ...input, -// response, -// } -// } -// case `memory`: { -// const result = await execute(input) -// return { -// ...input, -// result, -// } -// } -// default: -// throw casesExhausted(input) -// } -// }, -// }) -// .step({ -// name: `unpack`, -// run: async ({ input }) => { -// switch (input.transportType) { -// case `http`: { -// // todo 1 if response is missing header of content length then .json() hangs forever. -// // firstly consider a timeout, secondly, if response is malformed, then don't even run .json() -// // todo 2 if response is e.g. 404 with no json body, then an error is thrown because json parse cannot work, not gracefully handled here -// const json = await input.response.json() as object -// const result = parseExecutionResult(json) -// return { -// ...input, -// result, -// } -// } -// case `memory`: { -// return { -// ...input, -// result: input.result, -// } -// } -// default: -// throw casesExhausted(input) -// } -// }, -// }) -// .step({ -// name: `decode`, -// run: ({ input, previous }) => { -// // If there has been an error and we definitely don't have any data, such as when -// // giving an operation name that doesn't match any in the document, -// // then don't attempt to decode. -// const isError = !input.result.data && (input.result.errors?.length ?? 0) > 0 -// if (input.state.schemaMap && !isError) { -// decodeResultData({ -// sddm: input.state.schemaMap, -// request: normalizeRequestToNode(previous.pack.input.request), -// data: input.result.data, -// scalars: input.state.scalars.map, -// }) -// } + const baseProperties = mergeRequestInit( + mergeRequestInit( + mergeRequestInit( + { + headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec, + }, + { + headers: input.state.config.transport.config.headers, + }, + ), + input.state.config.transport.config.raw, + ), + { + headers: input.headers, + }, + ) + const request: + | RequestPipeline.Steps.CoreExchangePostRequest + | RequestPipeline.Steps.CoreExchangeGetRequest = requestMethod === `get` + ? { + methodMode: methodMode as MethodModeGetReads, + ...baseProperties, + method: `get`, + url: searchParamsAppendAll(input.url, slots.searchParams(graphqlRequest)), + } + : { + methodMode: methodMode, + ...baseProperties, + method: `post`, + url: input.url, + body: slots.body(graphqlRequest), + } + return { + ...input, + request, + } + } + default: + throw casesExhausted(input) + } + }, + }, { + name: `exchange`, + slots: { + fetch: (requestInfo: RequestInfo): MaybePromise => fetch(requestInfo), + }, + run: async ({ input, slots }) => { + switch (input.transportType) { + case `http`: { + const request = new Request(input.request.url, input.request) + const response = await slots.fetch(request) + return { + ...input, + response, + } + } + case `memory`: { + const result = await execute(input) + return { + ...input, + result, + } + } + default: + throw casesExhausted(input) + } + }, + }, { + name: `unpack`, + run: async ({ input }) => { + switch (input.transportType) { + case `http`: { + // todo 1 if response is missing header of content length then .json() hangs forever. + // firstly consider a timeout, secondly, if response is malformed, then don't even run .json() + // todo 2 if response is e.g. 404 with no json body, then an error is thrown because json parse cannot work, not gracefully handled here + const json = await input.response.json() as object + const result = parseExecutionResult(json) + return { + ...input, + result, + } + } + case `memory`: { + return { + ...input, + result: input.result, + } + } + default: + throw casesExhausted(input) + } + }, + }, { + name: `decode`, + run: ({ input, previous }) => { + // If there has been an error and we definitely don't have any data, such as when + // giving an operation name that doesn't match any in the document, + // then don't attempt to decode. + const isError = !input.result.data && (input.result.errors?.length ?? 0) > 0 + if (input.state.schemaMap && !isError) { + decodeResultData({ + sddm: input.state.schemaMap, + request: normalizeRequestToNode(previous.pack.input.request), + data: input.result.data, + scalars: input.state.scalars.map, + }) + } -// const result = input.transportType === `http` -// ? { -// ...input.result, -// response: input.response, -// } -// : input.result + const result = input.transportType === `http` + ? { + ...input.result, + response: input.response, + } + : input.result -// return result -// }, -// }) + return result + }, + }], + }) export namespace RequestPipeline { + export type Definition<$Config extends Config = Config> = Anyware.PipelineDefinition<[ + Steps.HookDefEncode<$Config>, + Steps.HookDefPack<$Config>, + Steps.HookDefExchange<$Config>, + Steps.HookDefUnpack<$Config>, + Steps.HookDefDecode<$Config>, + ]> + export namespace Steps { export interface HookInputBase { state: Context diff --git a/tests/_/SpyExtension.ts b/tests/_/SpyExtension.ts index eeb71db84..e710ebec5 100644 --- a/tests/_/SpyExtension.ts +++ b/tests/_/SpyExtension.ts @@ -1,7 +1,7 @@ import { beforeEach } from 'vitest' import { createExtension } from '../../src/entrypoints/main.js' import type { Config } from '../../src/entrypoints/utilities-for-generated.js' -import type { RequestPipeline } from '../../src/requestPipeline/__.js' +import type { RequestPipelineDefinition } from '../../src/requestPipeline/__.js' interface SpyData { encode: { From 280fd612ff88311a68c1fcd3e0a5b96920848b2e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 8 Nov 2024 21:57:32 -0500 Subject: [PATCH 18/36] work --- src/client/builderExtensions/anyware.ts | 6 +- src/client/client.transport-http.test.ts | 4 +- src/client/gql/gql.ts | 7 +- .../requestMethods/requestMethods.ts | 7 +- src/extension/extension.ts | 6 +- src/lib/anyware/ExecutableStep.ts | 5 + .../anyware/Interceptor/Interceptor.test-d.ts | 22 ++--- src/lib/anyware/Interceptor/Interceptor.ts | 12 +-- src/lib/anyware/Pipeline/Spec.test-d.ts | 12 +++ src/lib/anyware/Pipeline/Spec.ts | 47 ++++++++++ src/lib/anyware/Pipeline/_.ts | 3 +- src/lib/anyware/Pipeline/__.ts | 9 +- src/lib/anyware/Pipeline/builder.test-d.ts | 2 +- src/lib/anyware/Pipeline/builder.ts | 85 +++++++++-------- src/lib/anyware/Pipeline/createFromSpec.ts | 91 +++++++++++++++++++ src/lib/anyware/Pipeline/createFromType.ts | 67 -------------- src/lib/anyware/Pipeline/run.test-d.ts | 6 +- src/lib/anyware/Pipeline/run.ts | 7 +- src/lib/anyware/Pipeline/types.ts | 24 ----- src/lib/anyware/PipelineDefinition.ts | 5 - src/lib/anyware/Step.ts | 12 ++- src/lib/anyware/StepDefinition.ts | 8 -- src/lib/anyware/_.ts | 2 +- src/lib/anyware/__.entrypoint.test.ts | 2 +- src/lib/anyware/__.test-helpers.ts | 10 +- src/lib/anyware/run/runner.ts | 3 +- src/lib/prelude.ts | 1 + src/requestPipeline/RequestPipeline.ts | 14 +-- tests/_/SpyExtension.ts | 8 +- 29 files changed, 270 insertions(+), 217 deletions(-) create mode 100644 src/lib/anyware/ExecutableStep.ts create mode 100644 src/lib/anyware/Pipeline/Spec.test-d.ts create mode 100644 src/lib/anyware/Pipeline/Spec.ts create mode 100644 src/lib/anyware/Pipeline/createFromSpec.ts delete mode 100644 src/lib/anyware/Pipeline/createFromType.ts delete mode 100644 src/lib/anyware/Pipeline/types.ts delete mode 100644 src/lib/anyware/PipelineDefinition.ts delete mode 100644 src/lib/anyware/StepDefinition.ts diff --git a/src/client/builderExtensions/anyware.ts b/src/client/builderExtensions/anyware.ts index d4d27c529..a256a93b3 100644 --- a/src/client/builderExtensions/anyware.ts +++ b/src/client/builderExtensions/anyware.ts @@ -1,7 +1,7 @@ import { createExtension } from '../../extension/extension.js' import type { Anyware as AnywareLib } from '../../lib/anyware/__.js' import { Builder } from '../../lib/builder/__.js' -import type { RequestPipelineDefinition } from '../../requestPipeline/__.js' +import type { requestPipeline } from '../../requestPipeline/__.js' import { type Context } from '../context.js' export interface BuilderExtensionAnyware extends Builder.Extension { @@ -16,14 +16,14 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters + requestPipeline.Spec<$Arguments['context']['config']> >, ) => Builder.Definition.MaterializeWithNewContext<$Arguments['chain'], $Arguments['context']> } export const builderExtensionAnyware = Builder.Extension.create((builder, context) => { const properties = { - anyware: (interceptor: AnywareLib.Interceptor.InferConstructor) => { + anyware: (interceptor: AnywareLib.Interceptor.InferConstructor) => { return builder({ ...context, extensions: [ diff --git a/src/client/client.transport-http.test.ts b/src/client/client.transport-http.test.ts index ca3c79c75..925990a4d 100644 --- a/src/client/client.transport-http.test.ts +++ b/src/client/client.transport-http.test.ts @@ -5,7 +5,7 @@ import { Graffle as Pokemon } from '../../tests/_/schemas/pokemon/graffle/__.js' import { schema as schemaPokemon } from '../../tests/_/schemas/pokemon/schema.js' import { Graffle } from '../entrypoints/main.js' import { ACCEPT_REC, CONTENT_TYPE_REC } from '../lib/grafaid/http/http.js' -import type { RequestPipeline } from '../requestPipeline/__.js' +import type { requestPipeline } from '../requestPipeline/__.js' import { Transport, type TransportHttp } from '../types/Transport.js' const schema = new URL(`https://foo.io/api/graphql`) @@ -19,7 +19,7 @@ test(`anyware hooks are typed to http transport`, () => { expectTypeOf(exchange.input.transportType).toEqualTypeOf(Transport.http) // todo we can statically track the method mode like we do the transport mode expectTypeOf(exchange.input.request).toEqualTypeOf< - RequestPipeline.Steps.CoreExchangePostRequest | RequestPipeline.Steps.CoreExchangeGetRequest + requestPipeline.Steps.CoreExchangePostRequest | requestPipeline.Steps.CoreExchangeGetRequest >() const { unpack } = await exchange() expectTypeOf(unpack.input.transportType).toEqualTypeOf(Transport.http) diff --git a/src/client/gql/gql.ts b/src/client/gql/gql.ts index c1f68536c..dc3991101 100644 --- a/src/client/gql/gql.ts +++ b/src/client/gql/gql.ts @@ -7,10 +7,9 @@ import { joinTemplateStringArrayAndArgs, type TemplateStringsArguments, } from '../../lib/template-string.js' -import { RequestPipelineDefinition } from '../../requestPipeline/__.js' // todo +import { requestPipeline } from '../../requestPipeline/__.js' // todo import { type Context } from '../context.js' import { handleOutput } from '../handleOutput.js' -import type { Config } from '../Settings/Config.js' import { type DocumentController, resolveSendArguments, type sendArgumentsImplementation } from './send.js' export interface BuilderExtensionGql extends Builder.Extension { @@ -72,9 +71,9 @@ export const builderExtensionGql = Builder.Extension.create schema, // request, request: analyzedRequest, - } as RequestPipeline.Steps.HookDefEncode['input'] + } as requestPipeline.Steps.HookDefEncode['input'] - const result = await Anyware.Pipeline.run(RequestPipeline, { + const result = await Anyware.Pipeline.run(requestPipeline, { initialInput, // retryingExtension: context.retry as any, interceptors: context.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, diff --git a/src/documentBuilder/requestMethods/requestMethods.ts b/src/documentBuilder/requestMethods/requestMethods.ts index 6a080dfaa..d181baa38 100644 --- a/src/documentBuilder/requestMethods/requestMethods.ts +++ b/src/documentBuilder/requestMethods/requestMethods.ts @@ -2,14 +2,13 @@ import { OperationTypeNode } from 'graphql' import type { SimplifyDeep } from 'type-fest' import { type Context } from '../../client/context.js' import { handleOutput } from '../../client/handleOutput.js' -import type { Config } from '../../client/Settings/Config.js' import type { TypeFunction } from '../../entrypoints/utilities-for-generated.js' import { Anyware } from '../../lib/anyware/__.js' import { Builder } from '../../lib/builder/__.js' import type { Grafaid } from '../../lib/grafaid/__.js' import { getOperationDefinition } from '../../lib/grafaid/document.js' import { isSymbol } from '../../lib/prelude.js' -import { RequestPipelineDefinition } from '../../requestPipeline/__.js' +import { requestPipeline } from '../../requestPipeline/__.js' import type { GlobalRegistry } from '../../types/GlobalRegistry/GlobalRegistry.js' import { Select } from '../Select/__.js' import { SelectionSetGraphqlMapper } from '../SelectGraphQLMapper/__.js' @@ -136,9 +135,9 @@ const executeDocument = async ( url, schema, request, - } as RequestPipeline.Steps.HookDefEncode['input'] + } as requestPipeline.Steps.HookDefEncode['input'] - const result = await Anyware.Pipeline.run(RequestPipeline, { + const result = await Anyware.Pipeline.run(requestPipeline, { initialInput, // retryingExtension: state.retry as any, interceptors: state.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 286855e72..0b9813b89 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -8,7 +8,7 @@ import type { Builder } from '../lib/builder/__.js' import type { AssertExtends } from '../lib/prelude.js' import type { TypeFunction } from '../lib/type-function/__.js' import type { Fn } from '../lib/type-function/TypeFunction.js' -import type { RequestPipelineDefinition } from '../requestPipeline/__.js' +import type { requestPipeline } from '../requestPipeline/__.js' import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js' export interface TypeHooks { @@ -57,7 +57,7 @@ export interface Extension< /** * Anyware executed on every request. */ - onRequest?: Anyware.Interceptor.InferConstructor + onRequest?: Anyware.Interceptor.InferConstructor /** * Manipulate the builder. * You can extend the builder with new properties at both runtime AND buildtime (types, TypeScript). @@ -169,7 +169,7 @@ export const createExtension = < custom?: $Custom create: (params: { config: $Config }) => { builder?: $BuilderExtension - onRequest?: Anyware.Interceptor.InferConstructor + onRequest?: Anyware.Interceptor.InferConstructor typeHooks?: () => $TypeHooks } }, diff --git a/src/lib/anyware/ExecutableStep.ts b/src/lib/anyware/ExecutableStep.ts new file mode 100644 index 000000000..3f4f6e1b1 --- /dev/null +++ b/src/lib/anyware/ExecutableStep.ts @@ -0,0 +1,5 @@ +import type { Step } from './Step.js' + +export interface ExecutableStep extends Step { + run: (params: any) => any +} diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index fcd49f6f6..27c228e95 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -3,7 +3,6 @@ import { _, type ExcludeUndefined } from '../../prelude.js' import { type Interceptor, Pipeline } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' -import type { Builder } from '../Pipeline/builder.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' const p0 = Pipeline.create() @@ -14,7 +13,8 @@ describe(`interceptor constructor`, () => { .step({ name: `a`, run: () => results.a }) .step({ name: `b`, run: () => results.b }) .step({ name: `c`, run: () => results.c }) - type i1 = Interceptor.InferConstructor + .done() + type i1 = Interceptor.InferConstructor expectTypeOf>().toMatchTypeOf<[steps: { a: any; b: any; c: any }]>() expectTypeOf>().toMatchTypeOf<[steps: { a: (params: { input?: initialInput }) => Promise<{ b: (params: { input?: results['a'] }) => any }> @@ -26,13 +26,13 @@ describe(`interceptor constructor`, () => { // --- trigger --- test(`original input on self`, () => { - const p = p0.step({ name: `a`, run: () => results.a }) + const p = p0.step({ name: `a`, run: () => results.a }).done() type triggerA = GetTriggerFromBuilder expectTypeOf().toMatchTypeOf() }) test(`trigger arguments are optional`, () => { - const p = p0.step({ name: `a`, run: () => results.a }) + const p = p0.step({ name: `a`, run: () => results.a }).done() type triggerA = GetTriggerFromBuilder expectTypeOf<[]>().toMatchTypeOf>() }) @@ -40,7 +40,7 @@ describe(`interceptor constructor`, () => { // --- slots --- test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { - const p = p0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }) + const p = p0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }).done() type triggerA = GetTriggerFromBuilder type triggerB = GetTriggerFromBuilder expectTypeOf>().toEqualTypeOf<[params?: { @@ -54,14 +54,14 @@ describe(`interceptor constructor`, () => { }) test(`slots are optional`, () => { - const p = p0.step({ name: `a`, slots, run: () => results.a }) + const p = p0.step({ name: `a`, slots, run: () => results.a }).done() type triggerA = GetTriggerFromBuilder type triggerASlotInputs = ExcludeUndefined[0]>['using']> expectTypeOf<{ m?: any; n?: any }>().toMatchTypeOf() }) test(`slot function can return undefined (falls back to default slot)`, () => { - const p = p0.step({ name: `a`, slots, run: () => results.a }) + const p = p0.step({ name: `a`, slots, run: () => results.a }).done() type triggerA = GetTriggerFromBuilder type triggerASlotMOutput = ReturnType< ExcludeUndefined[0]>['using']>['m']> @@ -76,12 +76,12 @@ describe(`interceptor constructor`, () => { // --- output --- // test(`can return pipeline output or a step envelope`, () => { - const p = p0.step({ name: `a`, run: () => results.a }) + const p = p0.step({ name: `a`, run: () => results.a }).done() expectTypeOf>().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { - const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) + const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }).done() expectTypeOf>().toEqualTypeOf>() }) }) @@ -90,6 +90,6 @@ describe(`interceptor constructor`, () => { // dprint-ignore // @ts-expect-error -type GetTriggerFromBuilder<$Builder extends Builder, $TriggerName extends string> = Parameters>[0][$TriggerName] +type GetTriggerFromBuilder<$Pipeline extends Pipeline, $TriggerName extends string> = Parameters>[0][$TriggerName] // dprint-ignore -type GetReturnTypeFromBuilder<$Builder extends Builder> = ReturnType> +type GetReturnTypeFromBuilder<$Pipeline extends Pipeline> = ReturnType> diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index 71cbe3512..896135056 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,6 +1,6 @@ import type { Simplify } from 'type-fest' import type { Deferred, MaybePromise } from '../../prelude.js' -import type { Pipeline } from '../Pipeline/__.js' +import type { PipelineSpec } from '../_.js' import type { Step } from '../Step.js' import type { StepTrigger } from '../StepTrigger.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' @@ -19,20 +19,20 @@ export interface Interceptor { export namespace Interceptor { export interface InferConstructor< - $Pipeline extends Pipeline = Pipeline, + $PipelineSpec extends PipelineSpec = PipelineSpec, > // $Options extends InterceptorOptions = InterceptorOptions, { ( - steps: Simplify>, + steps: Simplify>, ): Promise< - | Pipeline.GetAwaitedResult<$Pipeline> + | Awaited<$PipelineSpec['output']> | StepTriggerEnvelope > } type InferConstructorKeywordArguments< - $Pipeline extends Pipeline, - > = InferConstructorKeywordArguments_<$Pipeline['steps'], Pipeline.GetAwaitedResult<$Pipeline>> + $PipelineSpec extends PipelineSpec, + > = InferConstructorKeywordArguments_<$PipelineSpec['steps'], Awaited<$PipelineSpec['output']>> // dprint-ignore type InferConstructorKeywordArguments_< diff --git a/src/lib/anyware/Pipeline/Spec.test-d.ts b/src/lib/anyware/Pipeline/Spec.test-d.ts new file mode 100644 index 000000000..f7cdef887 --- /dev/null +++ b/src/lib/anyware/Pipeline/Spec.test-d.ts @@ -0,0 +1,12 @@ +import { assertEqual } from '../../assert-equal.js' +import type { PipelineSpecFromSteps } from './Spec.js' + +assertEqual< + PipelineSpecFromSteps<[]>, + { steps: []; input: object; output: object } +>() + +assertEqual< + PipelineSpecFromSteps<[{ name: 'a' }]>, + { steps: [{ name: 'a'; slots: undefined; input: object; output: object }]; input: object; output: object } +>() diff --git a/src/lib/anyware/Pipeline/Spec.ts b/src/lib/anyware/Pipeline/Spec.ts new file mode 100644 index 000000000..8d7f5b2ec --- /dev/null +++ b/src/lib/anyware/Pipeline/Spec.ts @@ -0,0 +1,47 @@ +import type { IsUnknown } from 'type-fest' +import type { MaybePromise, Tuple } from '../../prelude.js' +import type { Step } from '../Step.js' + +// dprint-ignore +export type PipelineSpecFromSteps<$StepSpecInputs extends Step.SpecInput[] = Step.SpecInput[]> = PipelineSpec> + +// dprint-ignore +export interface PipelineSpec<$StepSpecs extends Step[] = Step[]> { + steps: $StepSpecs + input: $StepSpecs extends Tuple.NonEmpty + ? $StepSpecs[0]['input'] + : object + output: $StepSpecs extends Tuple.NonEmpty + ? Tuple.GetLastValue<$StepSpecs>['output'] + : object +} + +type InferStepSpecs<$StepSpecInputs extends Step.SpecInput[]> = InferStepSpecs_ + +// dprint-ignore +type InferStepSpecs_<$StepSpecPrevious extends Step| undefined, $StepSpecInputs extends Step.SpecInput[]> = + $StepSpecInputs extends [infer $StepSpecInput extends Step.SpecInput, ...infer $StepSpecInputsRest extends Step.SpecInput[]] + ? InferStepSpecs__<{ + name: $StepSpecInput['name'] + slots: IsUnknown<$StepSpecInput['slots']> extends true ? undefined : $StepSpecInput['slots'] + input: IsUnknown<$StepSpecInput['input']> extends true + ? $StepSpecPrevious extends Step + ? $StepSpecPrevious['output'] + : object + : $StepSpecInput['input'] + output: MaybePromise< + IsUnknown<$StepSpecInput['output']> extends true + ? $StepSpecInputsRest extends Tuple.NonEmpty + ? $StepSpecInputsRest[0]['input'] extends undefined + ? object + : $StepSpecInputsRest[0]['input'] + : object + : $StepSpecInput['output'] + > + }, $StepSpecInputsRest> + : [] + +type InferStepSpecs__<$StepSpec extends Step, $StepSpecInputsRest extends Step.SpecInput[]> = [ + $StepSpec, + ...InferStepSpecs_<$StepSpec, $StepSpecInputsRest>, +] diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index 217c9e1f8..792f74a46 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,3 +1,4 @@ export * from './builder.js' -export * from './createFromType.js' +export * from './createFromSpec.js' export * from './run.js' +export * from './Spec.js' diff --git a/src/lib/anyware/Pipeline/__.ts b/src/lib/anyware/Pipeline/__.ts index b8a07a76e..f7d910d2c 100644 --- a/src/lib/anyware/Pipeline/__.ts +++ b/src/lib/anyware/Pipeline/__.ts @@ -1,12 +1,11 @@ -import type { Step } from '../Step.js' +import type { ExecutableStep } from '../ExecutableStep.js' import type { Config } from './Config.js' export * as Pipeline from './_.js' export interface Pipeline { - input: object - // todo - // output: object - steps: Step[] config: Config + input: object + output: unknown + steps: ExecutableStep[] } diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/Pipeline/builder.test-d.ts index c80b4bda3..dbe35907e 100644 --- a/src/lib/anyware/Pipeline/builder.test-d.ts +++ b/src/lib/anyware/Pipeline/builder.test-d.ts @@ -2,7 +2,7 @@ import { expectTypeOf, test } from 'vitest' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import { Pipeline } from './__.js' -import type { Config } from './builder.js' +import type { Config } from './Config.js' const p0 = Pipeline.create() diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 15983635e..b3056a917 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -1,13 +1,13 @@ import type { ConfigManager } from '../../config-manager/__.js' import { type Tuple } from '../../prelude.js' +import type { ExecutableStep } from '../ExecutableStep.js' import type { Step } from '../Step.js' -import type { Pipeline } from './__.js' import { type Config, type Options, resolveOptions } from './Config.js' export interface Context { - input: object - steps: Step[] config: Config + input: object + steps: ExecutableStep[] } export interface ContextEmpty extends Context { @@ -16,40 +16,6 @@ export interface ContextEmpty extends Context { config: Config } -/** - * See {@link GetResult} - */ -export type GetAwaitedResult<$Pipeline extends Pipeline> = Awaited> - -/** - * Get the overall result of the pipeline. - * - * If the pipeline has no steps then the pipeline input itself. - * Otherwise the last step's output. - */ -// dprint-ignore -export type GetResult<$Pipeline extends Pipeline> = - $Pipeline['steps'] extends [any, ...any[]] - ? Step.GetResult> - : $Pipeline['input'] - -// dprint-ignore -export type GetNextStepParameterPrevious<$Pipeline extends Pipeline> = - $Pipeline['steps'] extends [any, ...any[]] - ? GetNextStepPrevious_<$Pipeline['steps']> - : undefined - -type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< - { - [$Index in keyof $Steps]: { - [$StepName in $Steps[$Index]['name']]: { - input: Parameters<$Steps[$Index]['run']>[0]['input'] - output: Awaited> - } - } - } -> - /** * Get the `input` parameter for a step that would be appended to the given Pipeline. * @@ -60,10 +26,10 @@ type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< * - Otherwise the last step's output. */ // dprint-ignore -type GetNextStepParameterInput<$Pipeline extends Pipeline> = - $Pipeline['steps'] extends [any, ...any[]] - ? Awaited>> - : $Pipeline['input'] +type GetNextStepParameterInput<$Context extends Context> = + $Context['steps'] extends Tuple.NonEmpty + ? Awaited['output']> + : $Context['input'] export interface Builder<$Context extends Context = Context> { context: $Context @@ -101,17 +67,50 @@ export interface Builder<$Context extends Context = Context> { ...$Context['steps'], { name: $Name - run: $Run input: $Params['input'] output: ReturnType<$Run> slots: $Slots + run: $Run }, ] > > + done: () => InferPipeline_<$Context> } -export type Infer<$Builder extends Builder> = $Builder['context'] +// dprint-ignore +export type GetNextStepParameterPrevious<$Context extends Context> = + $Context['steps'] extends Tuple.NonEmpty + ? GetNextStepPrevious_<$Context['steps']> + : undefined + +type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< + { + [$Index in keyof $Steps]: { + [$StepName in $Steps[$Index]['name']]: { + input: Awaited<$Steps[$Index]['input']> + output: Awaited<$Steps[$Index]['output']> + } + } + } +> + +export type InferPipeline<$Builder extends Builder> = InferPipeline_<$Builder['context']> + +// dprint-ignore +type InferPipeline_<$Context extends Context> = + & $Context + & { + /** + * The overall result of the pipeline. + * + * If the pipeline has no steps then is the pipeline input itself. + * Otherwise is the last step's output. + */ + output: $Context['steps'] extends Tuple.NonEmpty + ? Tuple.GetLastValue<$Context['steps']>['output'] + : $Context['input'] + } /** * TODO diff --git a/src/lib/anyware/Pipeline/createFromSpec.ts b/src/lib/anyware/Pipeline/createFromSpec.ts new file mode 100644 index 000000000..e57805b8f --- /dev/null +++ b/src/lib/anyware/Pipeline/createFromSpec.ts @@ -0,0 +1,91 @@ +import type { ConfigManager } from '../../config-manager/__.js' +import type { Tuple } from '../../prelude.js' +import type { Step } from '../Step.js' +import { type Config, type Options, resolveOptions } from './Config.js' +import type { PipelineSpec } from './Spec.js' + +export const createWithSpec = <$PipelineSpec extends PipelineSpec>( + input: { + options?: Options + steps: InferStepsInput<$PipelineSpec['steps']> + }, +): InferPipeline<$PipelineSpec> => { + const _config = resolveOptions(input.options) + input + return undefined as any +} + +type InferPipeline<$PipelineSpec extends PipelineSpec> = { + input: $PipelineSpec['input'] + output: $PipelineSpec['output'] + steps: InferExecutableSteps<$PipelineSpec['steps']> + config: Config +} + +type InferStepsInput<$NextStepDefinitions extends Step[]> = InferExecutableSteps_< + [], + $NextStepDefinitions, + { types: false } +> + +type InferExecutableSteps<$NextStepDefinitions extends Step[]> = InferExecutableSteps_< + [], + $NextStepDefinitions, + { types: true } +> + +// dprint-ignore +type InferExecutableSteps_< + $PreviousStepSpecs extends Step[], + $NextStepSpecs extends Step[], + $Options extends { types: boolean }, +> = + $NextStepSpecs extends [infer $StepSpec extends Step, ...infer $RestStepSpecs extends Step[]] + ? [ + & ( + $Options['types'] extends true + ? { + input: $StepSpec['input'] + output: $StepSpec['output'] + } + : {} + ) + & { + name: $StepSpec['name'] + run: (parameters: + & { + input: $StepSpec['input'] + previous: GetParameterPrevious<$PreviousStepSpecs> + } + & ( + $StepSpec['slots'] extends Step.Slots + ? { + slots: $StepSpec['slots'] + } + : {} + ) + ) => $StepSpec['output'] + } + & ( + $StepSpec['slots'] extends Step.Slots + ? { + slots: $StepSpec['slots'] + } + : {} + ) + , ...InferExecutableSteps_<[...$PreviousStepSpecs, $StepSpec], $RestStepSpecs, $Options>] + : [] + +export type GetParameterPrevious<$StepDefinitions extends Step[]> = Tuple.IntersectItems< + { + [$Index in keyof $StepDefinitions]: { + [$StepName in $StepDefinitions[$Index]['name']]: { + input: $StepDefinitions[$Index]['input'] + output: ConfigManager.OrDefault< + $StepDefinitions[$Index]['output'], + Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] + > + } + } + } +> diff --git a/src/lib/anyware/Pipeline/createFromType.ts b/src/lib/anyware/Pipeline/createFromType.ts deleted file mode 100644 index 19fd0daff..000000000 --- a/src/lib/anyware/Pipeline/createFromType.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ConfigManager } from '../../config-manager/__.js' -import type { Tuple } from '../../prelude.js' -import type { PipelineDefinition } from '../PipelineDefinition.js' -import type { Step } from '../Step.js' -import type { StepDefinition } from '../StepDefinition.js' -import { type Config, type Options, resolveOptions } from './Config.js' - -// dprint-ignore -type InferInputSteps<$PreviousStepDefinitions extends StepDefinition[], $NextStepDefinitions extends StepDefinition[]> = - $NextStepDefinitions extends [infer $StepDefinition extends StepDefinition, ...infer $RestStepDefinitions extends StepDefinition[]] - ? [ - & { - name: $StepDefinition['name'] - run: (parameters: - & { - input: $StepDefinition['input'] - previous: GetParameterPrevious<$PreviousStepDefinitions> - } - & ( - $StepDefinition['slots'] extends Step.Slots - ? { - slots: $StepDefinition['slots'] - } - : {} - ) - ) => $StepDefinition['output'] - } - & ( - $StepDefinition['slots'] extends Step.Slots - ? { - slots: $StepDefinition['slots'] - } - : {} - ) - , ...InferInputSteps<[...$PreviousStepDefinitions, $StepDefinition], $RestStepDefinitions>] - : [] - -export type GetParameterPrevious<$StepDefinitions extends StepDefinition[]> = Tuple.IntersectItems< - { - [$Index in keyof $StepDefinitions]: { - [$StepName in $StepDefinitions[$Index]['name']]: { - input: $StepDefinitions[$Index]['input'] - output: ConfigManager.OrDefault< - $StepDefinitions[$Index]['output'], - Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] - > - } - } - } -> - -type InferPipeline<$PipelineDefinition extends PipelineDefinition> = { - input: $PipelineDefinition['stepDefinitions'][0]['input'] - steps: InferInputSteps<[], $PipelineDefinition['stepDefinitions']> - config: Config -} - -export const createWithType = <$PipelineDefinition extends PipelineDefinition>( - input: { - options?: Options - steps: InferInputSteps<[], $PipelineDefinition['stepDefinitions']> - }, -): InferPipeline<$PipelineDefinition> => { - const _config = resolveOptions(input.options) - input - return undefined as any -} diff --git a/src/lib/anyware/Pipeline/run.test-d.ts b/src/lib/anyware/Pipeline/run.test-d.ts index af677736b..aad33f567 100644 --- a/src/lib/anyware/Pipeline/run.test-d.ts +++ b/src/lib/anyware/Pipeline/run.test-d.ts @@ -4,19 +4,19 @@ import type { initialInput } from '../__.test-helpers.js' import { Pipeline } from './__.js' test(`returns input if no steps`, async () => { - const p = Pipeline.create() + const p = Pipeline.create().done() const r = await Pipeline.run(p) expectTypeOf(r).toEqualTypeOf() }) test(`returns last step output if steps`, async () => { - const p = Pipeline.create().step({ name: `a`, run: () => 2 as const }) + const p = Pipeline.create().step({ name: `a`, run: () => 2 as const }).done() const r = await Pipeline.run(p) expectTypeOf(r).toEqualTypeOf<2 | ContextualAggregateError>() }) test(`can return a promise`, async () => { - const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(2 as const) }) + const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(2 as const) }).done() const r = await Pipeline.run(p) expectTypeOf(r).toEqualTypeOf<2 | ContextualAggregateError>() }) diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts index 96b46913d..3fa239a8c 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/Pipeline/run.ts @@ -1,7 +1,6 @@ import type { Errors } from '../../errors/__.js' import type { Interceptor } from '../Interceptor/Interceptor.js' import type { Pipeline } from './__.js' -import type { Builder } from './builder.js' interface Params { initialInput: object @@ -9,14 +8,14 @@ interface Params { } type Run = < - $Builder extends Builder, + $Pipeline extends Pipeline, $Params extends Params, >( - pipeline: $Builder, + pipeline: $Pipeline, params?: $Params, ) => Promise< | Errors.ContextualAggregateError - | Awaited> + | Awaited<$Pipeline['output']> > /** diff --git a/src/lib/anyware/Pipeline/types.ts b/src/lib/anyware/Pipeline/types.ts deleted file mode 100644 index ae3fa26b9..000000000 --- a/src/lib/anyware/Pipeline/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ConfigManager } from '../../config-manager/__.js' -import type { Tuple } from '../../prelude.js' -import type { StepDefinition } from '../StepDefinition.js' -import type { Pipeline } from './__.js' - -// dprint-ignore -export type GetNextStepParameterPrevious<$Pipeline extends Pipeline> = - $Pipeline['steps'] extends [any, ...any[]] - ? GetNextStepPrevious_<$Pipeline['steps']> - : undefined - -export type GetNextStepPrevious_<$StepDefinitions extends StepDefinition[]> = Tuple.IntersectItems< - { - [$Index in keyof $StepDefinitions]: { - [$StepName in $StepDefinitions[$Index]['name']]: { - input: $StepDefinitions[$Index]['input'] - output: ConfigManager.OrDefault< - $StepDefinitions[$Index]['output'], - Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] - > - } - } - } -> diff --git a/src/lib/anyware/PipelineDefinition.ts b/src/lib/anyware/PipelineDefinition.ts deleted file mode 100644 index bb67d4e87..000000000 --- a/src/lib/anyware/PipelineDefinition.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { StepDefinition } from './StepDefinition.js' - -export interface PipelineDefinition<$StepDefinitions extends StepDefinition[] = StepDefinition[]> { - stepDefinitions: $StepDefinitions -} diff --git a/src/lib/anyware/Step.ts b/src/lib/anyware/Step.ts index d3ef16de2..3d1253b57 100644 --- a/src/lib/anyware/Step.ts +++ b/src/lib/anyware/Step.ts @@ -7,10 +7,16 @@ export interface Step< slots?: Step.Slots input: any output: any - run: (params: any) => any } export namespace Step { + export interface SpecInput { + name: string + slots?: Step.Slots + input?: object + output?: unknown + } + /** * todo */ @@ -46,8 +52,4 @@ export namespace Step { export type Slots = Record export type Name = string - - export type GetAwaitedResult<$Step extends Step> = Awaited> - - export type GetResult<$Step extends Step> = ReturnType<$Step['run']> } diff --git a/src/lib/anyware/StepDefinition.ts b/src/lib/anyware/StepDefinition.ts deleted file mode 100644 index f5f4bc55b..000000000 --- a/src/lib/anyware/StepDefinition.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Step } from './Step.js' - -export type StepDefinition = { - name: string - slots?: Step.Slots - input?: Step.Input - output?: any -} diff --git a/src/lib/anyware/_.ts b/src/lib/anyware/_.ts index 131505d9f..a988c4cc0 100644 --- a/src/lib/anyware/_.ts +++ b/src/lib/anyware/_.ts @@ -1,4 +1,4 @@ export * from './Interceptor/Interceptor.js' export * from './Pipeline/__.js' -export * from './PipelineDefinition.js' +export * from './Pipeline/Spec.js' export * from './run/runner.js' diff --git a/src/lib/anyware/__.entrypoint.test.ts b/src/lib/anyware/__.entrypoint.test.ts index 8babc19e9..12d3a2982 100644 --- a/src/lib/anyware/__.entrypoint.test.ts +++ b/src/lib/anyware/__.entrypoint.test.ts @@ -7,7 +7,7 @@ import { type Interceptor, Pipeline } from './_.js' import { initialInput } from './__.test-helpers.js' const run = async (interceptor: (...args: any[]) => any) => { - const pipeline = Pipeline.create() + const pipeline = Pipeline.create().done() return Pipeline.run(pipeline, { initialInput, interceptors: [interceptor], diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index 0984730bb..d1a73e4e7 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -3,6 +3,7 @@ import { beforeEach, vi } from 'vitest' import type { Tuple } from '../prelude.js' import { Pipeline } from './_.js' import type { InterceptorInput } from './Interceptor/Interceptor.js' +import type { Options } from './Pipeline/Config.js' export const initialInput = { x: 1 } as const export type initialInput = typeof initialInput @@ -26,7 +27,7 @@ type PrivateHookRunnerInput = { previous: object } -export const createPipeline = (options?: Pipeline.Options) => { +export const createPipeline = (options?: Options) => { return Pipeline.create(options) .step({ name: `a`, @@ -58,6 +59,7 @@ export const createPipeline = (options?: Pipeline.Options) => { return { value: input.value + `+` + slots.append(`b`) + extra } }), }) + .done() } type TestBuilder = ReturnType @@ -66,11 +68,11 @@ type TestBuilder = ReturnType export let stepsIndex: Tuple.ToIndexByObjectKey = null beforeEach(() => { - const builder = createPipeline() - stepsIndex = keyBy(builder.context.steps, _ => _.name) as any + const pipeline = createPipeline() + stepsIndex = keyBy(pipeline.steps, _ => _.name) as any }) -export const runWithOptions = (options?: Pipeline.Options) => { +export const runWithOptions = (options?: Options) => { const builder = createPipeline(options) const run = async (...interceptors: InterceptorInput[]) => { return await Pipeline.run(builder, { diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index bf62480f6..1a6897a87 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -16,9 +16,10 @@ export const createRunner = <$Pipeline extends Pipeline>(pipeline: $Pipeline) => async ({ initialInput, interceptors, retryingInterceptor }: { initialInput: $Pipeline['input'] + // todo Pipeline needs to become sub-type of PipelineSpec then it should be accepted just fine. interceptors: Interceptor.InferConstructor<$Pipeline>[] retryingInterceptor?: Interceptor.InferConstructor<$Pipeline> - }): Promise | Errors.ContextualError> => { + }): Promise | Errors.ContextualError> => { const optimizedPipeline = optimizePipeline(pipeline) const interceptors_ = retryingInterceptor ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 6537f0723..4e1d051c8 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -300,6 +300,7 @@ export const debugSub = (...args: any[]) => (...subArgs: any[]) => { } export namespace Tuple { + export type NonEmpty = [any, ...any[]] // dprint-ignore export type IntersectItems<$Items extends readonly any[]> = $Items extends [infer $First, ...infer $Rest extends any[]] diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index aaaa0dfc7..b596ad449 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -19,11 +19,11 @@ import type { MethodModePost } from '../client/transportHttp/request.js' import type { httpMethodGet, httpMethodPost } from '../lib/http.js' import type { TransportHttp, TransportMemory } from '../types/Transport.js' -export const RequestPipeline = Anyware.Pipeline - .createWithType({ +export const requestPipeline = Anyware.Pipeline + .createWithSpec({ steps: [{ name: `encode`, - run: ({ input }): RequestPipeline.Steps.HookDefPack['input'] => { + run: ({ input }): requestPipeline.Steps.HookDefPack['input'] => { const sddm = input.state.schemaMap const scalars = input.state.scalars.map if (sddm) { @@ -88,8 +88,8 @@ export const RequestPipeline = Anyware.Pipeline }, ) const request: - | RequestPipeline.Steps.CoreExchangePostRequest - | RequestPipeline.Steps.CoreExchangeGetRequest = requestMethod === `get` + | requestPipeline.Steps.CoreExchangePostRequest + | requestPipeline.Steps.CoreExchangeGetRequest = requestMethod === `get` ? { methodMode: methodMode as MethodModeGetReads, ...baseProperties, @@ -191,8 +191,8 @@ export const RequestPipeline = Anyware.Pipeline }], }) -export namespace RequestPipeline { - export type Definition<$Config extends Config = Config> = Anyware.PipelineDefinition<[ +export namespace requestPipeline { + export type Spec<$Config extends Config = Config> = Anyware.PipelineSpecFromSteps<[ Steps.HookDefEncode<$Config>, Steps.HookDefPack<$Config>, Steps.HookDefExchange<$Config>, diff --git a/tests/_/SpyExtension.ts b/tests/_/SpyExtension.ts index e710ebec5..107d74713 100644 --- a/tests/_/SpyExtension.ts +++ b/tests/_/SpyExtension.ts @@ -1,17 +1,17 @@ import { beforeEach } from 'vitest' import { createExtension } from '../../src/entrypoints/main.js' import type { Config } from '../../src/entrypoints/utilities-for-generated.js' -import type { RequestPipelineDefinition } from '../../src/requestPipeline/__.js' +import type { requestPipeline } from '../../src/requestPipeline/__.js' interface SpyData { encode: { - input: RequestPipeline.Steps.HookDefEncode['input'] | null + input: requestPipeline.Steps.HookDefEncode['input'] | null } pack: { - input: RequestPipeline.Steps.HookDefPack['input'] | null + input: requestPipeline.Steps.HookDefPack['input'] | null } exchange: { - input: RequestPipeline.Steps.HookDefExchange['input'] | null + input: requestPipeline.Steps.HookDefExchange['input'] | null } } From 3545e434642b79ba8f85f61c88de9b0e72ec279d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 12:28:12 -0500 Subject: [PATCH 19/36] wip --- .../anyware/Interceptor/Interceptor.test-d.ts | 21 +++++----- src/lib/anyware/Interceptor/Interceptor.ts | 2 +- src/lib/anyware/Pipeline/Result.ts | 7 ++++ src/lib/anyware/Pipeline/Spec.test-d.ts | 40 ++++++++++++++++++- src/lib/anyware/Pipeline/Spec.ts | 24 ++++++----- src/lib/anyware/Pipeline/_.ts | 2 +- src/lib/anyware/Pipeline/__.ts | 16 ++++++-- src/lib/anyware/Pipeline/builder.test-d.ts | 24 +++++++---- src/lib/anyware/Pipeline/builder.ts | 20 +++++++--- .../anyware/Pipeline/createWithSpec.test-d.ts | 28 +++++++++++++ .../{createFromSpec.ts => createWithSpec.ts} | 0 src/lib/anyware/Pipeline/run.test-d.ts | 8 ++-- src/lib/anyware/Pipeline/run.ts | 5 +-- src/lib/anyware/__.test-helpers.ts | 5 +++ src/lib/anyware/__.test.ts | 6 +-- src/lib/anyware/run/OptimizedPipeline.ts | 4 +- src/requestPipeline/RequestPipeline.ts | 1 + tests/_/helpers.ts | 32 ++++++++++++++- 18 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 src/lib/anyware/Pipeline/Result.ts create mode 100644 src/lib/anyware/Pipeline/createWithSpec.test-d.ts rename src/lib/anyware/Pipeline/{createFromSpec.ts => createWithSpec.ts} (100%) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 27c228e95..8ffe8baea 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -27,13 +27,13 @@ describe(`interceptor constructor`, () => { test(`original input on self`, () => { const p = p0.step({ name: `a`, run: () => results.a }).done() - type triggerA = GetTriggerFromBuilder + type triggerA = GetTriggerFromPipeline expectTypeOf().toMatchTypeOf() }) test(`trigger arguments are optional`, () => { const p = p0.step({ name: `a`, run: () => results.a }).done() - type triggerA = GetTriggerFromBuilder + type triggerA = GetTriggerFromPipeline expectTypeOf<[]>().toMatchTypeOf>() }) @@ -41,8 +41,8 @@ describe(`interceptor constructor`, () => { test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { const p = p0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }).done() - type triggerA = GetTriggerFromBuilder - type triggerB = GetTriggerFromBuilder + type triggerA = GetTriggerFromPipeline + type triggerB = GetTriggerFromPipeline expectTypeOf>().toEqualTypeOf<[params?: { input?: initialInput using?: { @@ -55,14 +55,14 @@ describe(`interceptor constructor`, () => { test(`slots are optional`, () => { const p = p0.step({ name: `a`, slots, run: () => results.a }).done() - type triggerA = GetTriggerFromBuilder + type triggerA = GetTriggerFromPipeline type triggerASlotInputs = ExcludeUndefined[0]>['using']> expectTypeOf<{ m?: any; n?: any }>().toMatchTypeOf() }) test(`slot function can return undefined (falls back to default slot)`, () => { const p = p0.step({ name: `a`, slots, run: () => results.a }).done() - type triggerA = GetTriggerFromBuilder + type triggerA = GetTriggerFromPipeline type triggerASlotMOutput = ReturnType< ExcludeUndefined[0]>['using']>['m']> > @@ -77,12 +77,13 @@ describe(`interceptor constructor`, () => { // test(`can return pipeline output or a step envelope`, () => { const p = p0.step({ name: `a`, run: () => results.a }).done() - expectTypeOf>().toEqualTypeOf>() + type i = GetReturnTypeFromPipeline + expectTypeOf().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }).done() - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) @@ -90,6 +91,6 @@ describe(`interceptor constructor`, () => { // dprint-ignore // @ts-expect-error -type GetTriggerFromBuilder<$Pipeline extends Pipeline, $TriggerName extends string> = Parameters>[0][$TriggerName] +type GetTriggerFromPipeline<$Pipeline extends Pipeline, $TriggerName extends string> = Parameters>[0][$TriggerName] // dprint-ignore -type GetReturnTypeFromBuilder<$Pipeline extends Pipeline> = ReturnType> +type GetReturnTypeFromPipeline<$Pipeline extends Pipeline> = ReturnType> diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index 896135056..ef60fa4db 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -25,7 +25,7 @@ export namespace Interceptor { ( steps: Simplify>, ): Promise< - | Awaited<$PipelineSpec['output']> + | $PipelineSpec['output'] | StepTriggerEnvelope > } diff --git a/src/lib/anyware/Pipeline/Result.ts b/src/lib/anyware/Pipeline/Result.ts new file mode 100644 index 000000000..9efe40f11 --- /dev/null +++ b/src/lib/anyware/Pipeline/Result.ts @@ -0,0 +1,7 @@ +import type { Errors } from '../../errors/__.js' + +export type Result = Errors.ContextualAggregateError | SuccessfulResult + +export interface SuccessfulResult { + value: T +} diff --git a/src/lib/anyware/Pipeline/Spec.test-d.ts b/src/lib/anyware/Pipeline/Spec.test-d.ts index f7cdef887..5567a7b19 100644 --- a/src/lib/anyware/Pipeline/Spec.test-d.ts +++ b/src/lib/anyware/Pipeline/Spec.test-d.ts @@ -1,12 +1,48 @@ import { assertEqual } from '../../assert-equal.js' +import type { MaybePromise } from '../../prelude.js' import type { PipelineSpecFromSteps } from './Spec.js' assertEqual< PipelineSpecFromSteps<[]>, - { steps: []; input: object; output: object } + { steps: []; input: object; output: unknown } >() assertEqual< PipelineSpecFromSteps<[{ name: 'a' }]>, - { steps: [{ name: 'a'; slots: undefined; input: object; output: object }]; input: object; output: object } + { steps: [{ name: 'a'; slots: undefined; input: object; output: unknown }]; input: object; output: unknown } +>() + +assertEqual< + PipelineSpecFromSteps<[{ name: 'a'; output: 1 }]>, + { + steps: [{ name: 'a'; slots: undefined; input: object; output: MaybePromise<1> }] + input: object + output: 1 + // ^^^^^^ pipeline output inferred from last step output + } +>() + +assertEqual< + PipelineSpecFromSteps<[{ name: 'a' }, { name: 'b'; input: { x: 1 } }]>, + { + steps: [ + { name: 'a'; slots: undefined; input: object; output: MaybePromise<{ x: 1 }> }, + // step output inferred from next step input ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + { name: 'b'; slots: undefined; input: { x: 1 }; output: unknown }, + ] + input: object + output: unknown + } +>() + +assertEqual< + PipelineSpecFromSteps<[{ name: 'a'; output: Promise<1> }]>, + // ^^^^^^^^^^^^^^^^^^ Step promised output flattened within MaybePromise + { + steps: [{ name: 'a'; slots: undefined; input: object; output: MaybePromise<1> }] + // ^^^^^^^^^^^^^^^ + input: object + output: 1 + // ^^^^^^^^^ Pipeline output strips Promise type + } >() diff --git a/src/lib/anyware/Pipeline/Spec.ts b/src/lib/anyware/Pipeline/Spec.ts index 8d7f5b2ec..25c4dc9cb 100644 --- a/src/lib/anyware/Pipeline/Spec.ts +++ b/src/lib/anyware/Pipeline/Spec.ts @@ -11,9 +11,11 @@ export interface PipelineSpec<$StepSpecs extends Step[] = Step[]> { input: $StepSpecs extends Tuple.NonEmpty ? $StepSpecs[0]['input'] : object - output: $StepSpecs extends Tuple.NonEmpty - ? Tuple.GetLastValue<$StepSpecs>['output'] - : object + output: Awaited< + $StepSpecs extends Tuple.NonEmpty + ? Tuple.GetLastValue<$StepSpecs>['output'] + : unknown + > } type InferStepSpecs<$StepSpecInputs extends Step.SpecInput[]> = InferStepSpecs_ @@ -30,13 +32,15 @@ type InferStepSpecs_<$StepSpecPrevious extends Step| undefined, $StepSpecInputs : object : $StepSpecInput['input'] output: MaybePromise< - IsUnknown<$StepSpecInput['output']> extends true - ? $StepSpecInputsRest extends Tuple.NonEmpty - ? $StepSpecInputsRest[0]['input'] extends undefined - ? object - : $StepSpecInputsRest[0]['input'] - : object - : $StepSpecInput['output'] + Awaited< + IsUnknown<$StepSpecInput['output']> extends true + ? $StepSpecInputsRest extends Tuple.NonEmpty + ? $StepSpecInputsRest[0]['input'] extends undefined + ? unknown + : $StepSpecInputsRest[0]['input'] + : unknown + : $StepSpecInput['output'] + > > }, $StepSpecInputsRest> : [] diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index 792f74a46..e8c9f48aa 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,4 +1,4 @@ export * from './builder.js' -export * from './createFromSpec.js' +export * from './createWithSpec.js' export * from './run.js' export * from './Spec.js' diff --git a/src/lib/anyware/Pipeline/__.ts b/src/lib/anyware/Pipeline/__.ts index f7d910d2c..626718785 100644 --- a/src/lib/anyware/Pipeline/__.ts +++ b/src/lib/anyware/Pipeline/__.ts @@ -1,11 +1,21 @@ import type { ExecutableStep } from '../ExecutableStep.js' import type { Config } from './Config.js' +import type { PipelineSpec } from './Spec.js' export * as Pipeline from './_.js' -export interface Pipeline { +// todo reconsider the division between these two types. +// Spec is fully declarative while this has "executable" steps +// which are spec steps with function signatures +// The problem: steps become executable but output remains in its declarative form +// One could also rename this type to "ExecutablePipeline" +// Where is this type _actually_ needed? The executable from seems to be a implementation detail? +// Can the external interface be just the spec type? +// The builder by definition includes the executable steps and infers the spec _from that_. So +// in that case its understandable. How about we introduce an "InferSpec" type + make executable pipeline +// have a spec property. Then use of utility types can go like: Anyware.Interceptor.Infer +// or make utility types accept spec OR executable and do the conditional branch of looking up ['spec'] within. +export interface Pipeline extends PipelineSpec { config: Config - input: object - output: unknown steps: ExecutableStep[] } diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/Pipeline/builder.test-d.ts index dbe35907e..30a50c6aa 100644 --- a/src/lib/anyware/Pipeline/builder.test-d.ts +++ b/src/lib/anyware/Pipeline/builder.test-d.ts @@ -1,23 +1,23 @@ import { expectTypeOf, test } from 'vitest' import type { initialInput } from '../__.test-helpers.js' -import { results, slots } from '../__.test-helpers.js' +import { results, slots, stepA } from '../__.test-helpers.js' import { Pipeline } from './__.js' import type { Config } from './Config.js' -const p0 = Pipeline.create() +const b0 = Pipeline.create() test(`initial context`, () => { - expectTypeOf(p0.context).toEqualTypeOf<{ input: initialInput; steps: []; config: Config }>() + expectTypeOf(b0.context).toEqualTypeOf<{ input: initialInput; steps: []; config: Config }>() }) test(`first step definition`, () => { - expectTypeOf(p0.step).toMatchTypeOf< + expectTypeOf(b0.step).toMatchTypeOf< (input: { name: string; run: (params: { input: initialInput; previous: undefined }) => any }) => any >() }) test(`second step definition`, () => { - const p1 = p0.step({ name: `a`, run: () => results.a }) + const p1 = b0.step({ name: `a`, run: () => results.a }) expectTypeOf(p1.step).toMatchTypeOf< ( input: { @@ -39,13 +39,13 @@ test(`second step definition`, () => { >() }) test(`step input receives awaited return value from previous step `, () => { - const p1 = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }) + const p1 = b0.step({ name: `a`, run: () => Promise.resolve(results.a) }) type s2Parameters = Parameters[0]['run']>[0]['input'] expectTypeOf().toEqualTypeOf() }) test(`step definition with slots`, () => { - const p1 = p0 + const p1 = b0 .step({ name: `a`, slots: { @@ -74,3 +74,13 @@ test(`step definition with slots`, () => { } >() }) + +test(`.done() returns a pipeline`, () => { + const p0 = b0.done() + expectTypeOf().toMatchTypeOf<{ config: Config; steps: []; input: initialInput; output: initialInput }>() + + const p1 = b0.step(stepA).done() + expectTypeOf().toMatchTypeOf< + { config: Config; steps: [{ name: `a`; run: any }]; input: initialInput; output: results['a'] } + >() +}) diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index b3056a917..04b8799d0 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -3,6 +3,7 @@ import { type Tuple } from '../../prelude.js' import type { ExecutableStep } from '../ExecutableStep.js' import type { Step } from '../Step.js' import { type Config, type Options, resolveOptions } from './Config.js' +import type { Result } from './Result.js' export interface Context { config: Config @@ -75,7 +76,7 @@ export interface Builder<$Context extends Context = Context> { ] > > - done: () => InferPipeline_<$Context> + done: () => InferPipelineFromContext<$Context> } // dprint-ignore @@ -95,10 +96,10 @@ type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< } > -export type InferPipeline<$Builder extends Builder> = InferPipeline_<$Builder['context']> +export type InferPipeline<$Builder extends Builder> = InferPipelineFromContext<$Builder['context']> // dprint-ignore -type InferPipeline_<$Context extends Context> = +type InferPipelineFromContext<$Context extends Context> = & $Context & { /** @@ -107,9 +108,16 @@ type InferPipeline_<$Context extends Context> = * If the pipeline has no steps then is the pipeline input itself. * Otherwise is the last step's output. */ - output: $Context['steps'] extends Tuple.NonEmpty - ? Tuple.GetLastValue<$Context['steps']>['output'] - : $Context['input'] + output: + // Promise< + // Result< + Awaited< + $Context['steps'] extends Tuple.NonEmpty + ? Tuple.GetLastValue<$Context['steps']>['output'] + : $Context['input'] + > + // > + // > } /** diff --git a/src/lib/anyware/Pipeline/createWithSpec.test-d.ts b/src/lib/anyware/Pipeline/createWithSpec.test-d.ts new file mode 100644 index 000000000..c5217aac0 --- /dev/null +++ b/src/lib/anyware/Pipeline/createWithSpec.test-d.ts @@ -0,0 +1,28 @@ +import { assertEqual } from '../../assert-equal.js' +import type { MaybePromise } from '../../prelude.js' +import type { Config } from './Config.js' +import { createWithSpec } from './createWithSpec.js' +import type { PipelineSpecFromSteps } from './Spec.js' + +{ + type Spec = PipelineSpecFromSteps<[]> + const p = createWithSpec({ steps: [] }) + assertEqual() +} + +{ + type Spec = PipelineSpecFromSteps<[{ name: 'a'; output: 1 }]> + const p = createWithSpec({ steps: [{ name: `a`, run: () => 1 }] }) + type p = typeof p + assertEqual< + p, + { + config: Config + input: object + output: 1 + steps: [ + { run: (...arg: any[]) => any; name: `a`; input: object; output: MaybePromise<1> }, + ] + } + >() +} diff --git a/src/lib/anyware/Pipeline/createFromSpec.ts b/src/lib/anyware/Pipeline/createWithSpec.ts similarity index 100% rename from src/lib/anyware/Pipeline/createFromSpec.ts rename to src/lib/anyware/Pipeline/createWithSpec.ts diff --git a/src/lib/anyware/Pipeline/run.test-d.ts b/src/lib/anyware/Pipeline/run.test-d.ts index aad33f567..cb1ba19c7 100644 --- a/src/lib/anyware/Pipeline/run.test-d.ts +++ b/src/lib/anyware/Pipeline/run.test-d.ts @@ -1,22 +1,22 @@ import { expectTypeOf, test } from 'vitest' -import type { ContextualAggregateError } from '../../errors/ContextualAggregateError.js' import type { initialInput } from '../__.test-helpers.js' import { Pipeline } from './__.js' +import type { Result } from './Result.js' test(`returns input if no steps`, async () => { const p = Pipeline.create().done() const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf() + expectTypeOf(r).toEqualTypeOf>() }) test(`returns last step output if steps`, async () => { const p = Pipeline.create().step({ name: `a`, run: () => 2 as const }).done() const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf<2 | ContextualAggregateError>() + expectTypeOf(r).toEqualTypeOf>() }) test(`can return a promise`, async () => { const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(2 as const) }).done() const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf<2 | ContextualAggregateError>() + expectTypeOf(r).toEqualTypeOf>() }) diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts index 3fa239a8c..93d4eda19 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/Pipeline/run.ts @@ -1,6 +1,6 @@ -import type { Errors } from '../../errors/__.js' import type { Interceptor } from '../Interceptor/Interceptor.js' import type { Pipeline } from './__.js' +import type { Result } from './Result.js' interface Params { initialInput: object @@ -14,8 +14,7 @@ type Run = < pipeline: $Pipeline, params?: $Params, ) => Promise< - | Errors.ContextualAggregateError - | Awaited<$Pipeline['output']> + Result<$Pipeline['output']> > /** diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index d1a73e4e7..a8e851a79 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -4,6 +4,7 @@ import type { Tuple } from '../prelude.js' import { Pipeline } from './_.js' import type { InterceptorInput } from './Interceptor/Interceptor.js' import type { Options } from './Pipeline/Config.js' +import { Step } from './Step.js' export const initialInput = { x: 1 } as const export type initialInput = typeof initialInput @@ -15,6 +16,10 @@ export const results = { } as const export type results = typeof results +export const stepA = Step.createWithInput()({ name: `a`, run: () => results[`a`] }) +export const stepB = Step.createWithInput()({ name: `b`, run: () => results[`b`] }) +export const stepC = Step.createWithInput()({ name: `c`, run: () => results[`c`] }) + export const slots = { m: () => Promise.resolve(`m` as const), n: () => `n` as const, diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts index bf2f8f169..89845c91a 100644 --- a/src/lib/anyware/__.test.ts +++ b/src/lib/anyware/__.test.ts @@ -258,7 +258,7 @@ describe(`errors`, () => { test('via passthroughErrorInstanceOf (one)', async () => { const builder = Pipeline.create<{ throws: Error }>({ passthroughErrorInstanceOf: [SpecialError1], - }).step(stepA) + }).step(stepA).done() // dprint-ignore expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) @@ -268,7 +268,7 @@ describe(`errors`, () => { test('via passthroughErrorInstanceOf (multiple)', async () => { const builder = Pipeline.create<{ throws: Error }>({ passthroughErrorInstanceOf: [SpecialError1, SpecialError2], - }).step(stepA) + }).step(stepA).done() // dprint-ignore expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) // dprint-ignore @@ -281,7 +281,7 @@ describe(`errors`, () => { passthroughErrorWith: (signal) => { return signal.error instanceof SpecialError1 }, - }).step(stepA) + }).step(stepA).done() // dprint-ignore expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) // dprint-ignore diff --git a/src/lib/anyware/run/OptimizedPipeline.ts b/src/lib/anyware/run/OptimizedPipeline.ts index 8e849c3ec..1c818173d 100644 --- a/src/lib/anyware/run/OptimizedPipeline.ts +++ b/src/lib/anyware/run/OptimizedPipeline.ts @@ -1,11 +1,13 @@ +import type { ExecutableStep } from '../ExecutableStep.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Step.js' -export type StepsIndex = Map +export type StepsIndex = Map export interface OptimizedPipeline extends Pipeline { stepsIndex: StepsIndex } + export const optimizePipeline = (pipeline: Pipeline): OptimizedPipeline => { const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) return { ...pipeline, stepsIndex } diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index b596ad449..4ce239021 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -291,6 +291,7 @@ export namespace requestPipeline { { response: Response } > & { result: FormattedExecutionResult } + output: 'todo' } /** diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts index fd7f2396a..d4247c06d 100644 --- a/tests/_/helpers.ts +++ b/tests/_/helpers.ts @@ -5,6 +5,7 @@ import * as Path from 'node:path' import type { Mock } from 'vitest' import { test as testBase, vi } from 'vitest' import type { Client } from '../../src/client/client.js' +import type { TransportConfigHttp, TransportConfigMemory } from '../../src/client/Settings/Config.js' import { Graffle } from '../../src/entrypoints/main.js' import type { Context, SchemaDrivenDataMap } from '../../src/entrypoints/utilities-for-generated.js' import type { ConfigManager } from '../../src/lib/config-manager/__.js' @@ -37,8 +38,32 @@ interface Fixtures { fetch: Mock<(request: Request) => Promise> pokemonService: SchemaService graffle: Client - kitchenSink: Client> - kitchenSinkHttp: Client> + kitchenSink: Client< + ConfigManager.SetProperties< + Context, + { + name: `default` + schemaMap: SchemaDrivenDataMap + config: { + output: Context['config']['output'] + transport: TransportConfigMemory + } + } + > + > + kitchenSinkHttp: Client< + ConfigManager.SetProperties< + Context, + { + name: `default` + schemaMap: SchemaDrivenDataMap + config: { + output: Context['config']['output'] + transport: TransportConfigHttp + } + } + > + > kitchenSinkData: typeof db project: Project } @@ -116,6 +141,9 @@ export const test = testBase.extend({ }, kitchenSink: async ({ fetch: _ }, use) => { const kitchenSink = KitchenSink.create({ schema: kitchenSinkSchema }) + // kitchenSink.anyware(async ({ encode }) => { + // encode({ input: {}}) + // }) await use(kitchenSink) }, kitchenSinkHttp: async ({ fetch: _ }, use) => { From 16b755942a0a186de98a2093bc6be434029391af Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 12:47:53 -0500 Subject: [PATCH 20/36] no type errors! --- .../client.create.config.output.test-d.ts | 12 ++++--- src/client/handleOutput.ts | 31 +++++++------------ src/lib/anyware/Pipeline/Result.ts | 9 ++++-- src/lib/anyware/Pipeline/_.ts | 1 + src/requestPipeline/RequestPipeline.ts | 23 +++++++++----- 5 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/client/Settings/client.create.config.output.test-d.ts b/src/client/Settings/client.create.config.output.test-d.ts index d5c4b038e..3b10c883a 100644 --- a/src/client/Settings/client.create.config.output.test-d.ts +++ b/src/client/Settings/client.create.config.output.test-d.ts @@ -6,7 +6,7 @@ import { Graffle } from '../../../tests/_/schemas/kitchen-sink/graffle/__.js' import { schema } from '../../../tests/_/schemas/kitchen-sink/schema.js' import { assertEqual } from '../../lib/assert-equal.js' import { type GraphQLExecutionResultError } from '../../lib/grafaid/graphql.js' -import type { ErrorsOther } from '../handleOutput.js' +import type { requestPipeline } from '../../requestPipeline/RequestPipeline.js' const G = Graffle.create @@ -99,7 +99,7 @@ describe('.envelope', () => { describe('.errors', () => { test('defaults to execution errors in envelope', () => { const g = G({ schema, output: { defaults: { errorChannel: 'return' }, envelope: true } }) - expectTypeOf(g.query.__typename()).resolves.toMatchTypeOf | ErrorsOther>() + expectTypeOf(g.query.__typename()).resolves.toMatchTypeOf | requestPipeline.ResultFailure>() }) test('.execution:false restores errors to return', async () => { const g = G({ @@ -107,7 +107,7 @@ describe('.envelope', () => { output: { defaults: { errorChannel: 'return' }, envelope: { errors: { execution: false } } }, }) expectTypeOf(await g.query.__typename()).toEqualTypeOf< - Omit, 'errors'> | ErrorsOther | GraphQLExecutionResultError + Omit, 'errors'> | requestPipeline.ResultFailure | GraphQLExecutionResultError >() }) test('.other:true raises them to envelope', () => { @@ -135,7 +135,9 @@ describe('defaults.errorChannel: "return"', () => { describe('puts errors into return type', () => { const g = G({ schema, output: { defaults: { errorChannel: 'return' } } }) test('query.', async () => { - expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | ErrorsOther | GraphQLExecutionResultError>() + expectTypeOf(await g.query.__typename()).toEqualTypeOf< + 'Query' | requestPipeline.ResultFailure | GraphQLExecutionResultError + >() }) }) describe('with .errors', () => { @@ -144,7 +146,7 @@ describe('defaults.errorChannel: "return"', () => { schema, output: { defaults: { errorChannel: 'return' }, errors: { execution: 'throw' } }, }) - expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | ErrorsOther>() + expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | requestPipeline.ResultFailure>() }) test('.other: throw', async () => { const g = G({ diff --git a/src/client/handleOutput.ts b/src/client/handleOutput.ts index d0d522194..31b2d9a71 100644 --- a/src/client/handleOutput.ts +++ b/src/client/handleOutput.ts @@ -11,6 +11,7 @@ import { type GetOrNever, type Values, } from '../lib/prelude.js' +import type { requestPipeline } from '../requestPipeline/RequestPipeline.js' import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js' import type { TransportHttp } from '../types/Transport.js' import type { Context } from './context.js' @@ -22,14 +23,6 @@ import { readConfigErrorCategoryOutputChannel, } from './Settings/Config.js' -/** - * Types of "other" Graffle Error. - */ -export type ErrorsOther = - | Errors.ContextualError - // Possible from http transport fetch with abort controller. - | DOMException - export type GraffleExecutionResultEnvelope<$Config extends Config = Config> = // & ExecutionResult & { @@ -56,13 +49,13 @@ export type GraffleExecutionResultEnvelope<$Config extends Config = Config> = } : {}) -export type GraffleExecutionResultVar<$Config extends Config = Config> = - | GraffleExecutionResultEnvelope<$Config> - | ErrorsOther +// export type GraffleExecutionResultVar<$Config extends Config = Config> = +// | GraffleExecutionResultEnvelope<$Config> +// | ErrorsOther export const handleOutput = ( state: Context, - result: GraffleExecutionResultVar, + result: requestPipeline.Result, ) => { if (isContextConfigTraditionalGraphQLOutput(state.config)) { if (result instanceof Error) throw result @@ -93,11 +86,11 @@ export const handleOutput = ( return isEnvelope ? { errors: [result] } : result } - if (result.errors && result.errors.length > 0) { + if (result.value.errors && result.value.errors.length > 0) { const error = new Errors.ContextualAggregateError( `One or more errors in the execution result.`, {}, - result.errors.map(e => { + result.value.errors.map(e => { if (e instanceof Error) return e const { message, ...context } = e return new Errors.ContextualError(message, context) @@ -105,14 +98,14 @@ export const handleOutput = ( ) if (isThrowExecution) throw error if (isReturnExecution) return error - return isEnvelope ? { ...result, errors: [...result.errors ?? [], error] } : error + return isEnvelope ? { ...result.value, errors: [...result.value.errors ?? [], error] } : error } if (isEnvelope) { - return result + return result.value } - return result.data + return result.value.data } /** @@ -197,8 +190,8 @@ type HandleOutput_Envelope<$Context extends Context, $Envelope extends GraffleEx // dprint-ignore type IfConfiguredGetOutputErrorReturns<$Context extends Context> = - | (ConfigGetOutputError<$Context, 'execution'> extends 'return' ? GraphQLExecutionResultError : never) - | (ConfigGetOutputError<$Context, 'other'> extends 'return' ? ErrorsOther : never) + | (ConfigGetOutputError<$Context, 'execution'> extends 'return' ? GraphQLExecutionResultError : never) + | (ConfigGetOutputError<$Context, 'other'> extends 'return' ? requestPipeline.ResultFailure : never) // dprint-ignore export type ConfigGetOutputError<$Context extends Context, $ErrorCategory extends ErrorCategory> = diff --git a/src/lib/anyware/Pipeline/Result.ts b/src/lib/anyware/Pipeline/Result.ts index 9efe40f11..1019ea209 100644 --- a/src/lib/anyware/Pipeline/Result.ts +++ b/src/lib/anyware/Pipeline/Result.ts @@ -1,7 +1,12 @@ import type { Errors } from '../../errors/__.js' +import type { PipelineSpec } from './Spec.js' -export type Result = Errors.ContextualAggregateError | SuccessfulResult +export type InferResultFromSpec<$PipelineSpec extends PipelineSpec> = Result<$PipelineSpec['output']> -export interface SuccessfulResult { +export type ResultFailure = Errors.ContextualAggregateError + +export type Result = ResultFailure | ResultSuccess + +export interface ResultSuccess { value: T } diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index e8c9f48aa..48b57fbf5 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,4 +1,5 @@ export * from './builder.js' export * from './createWithSpec.js' +export * from './Result.js' export * from './run.js' export * from './Spec.js' diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index 4ce239021..cb87bd129 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -1,4 +1,9 @@ +import type { FormattedExecutionResult, GraphQLSchema } from 'graphql' +import type { Context } from '../client/context.js' +import type { GraffleExecutionResultEnvelope } from '../client/handleOutput.js' +import type { Config } from '../client/Settings/Config.js' import { MethodMode, type MethodModeGetReads } from '../client/transportHttp/request.js' +import type { MethodModePost } from '../client/transportHttp/request.js' import { Anyware } from '../lib/anyware/__.js' import type { Grafaid } from '../lib/grafaid/__.js' import { OperationTypeToAccessKind, print } from '../lib/grafaid/document.js' @@ -7,18 +12,13 @@ import { getRequestEncodeSearchParameters, postRequestEncodeBody } from '../lib/ import { getRequestHeadersRec, parseExecutionResult, postRequestHeadersRec } from '../lib/grafaid/http/http.js' import { normalizeRequestToNode } from '../lib/grafaid/request.js' import { mergeRequestInit, searchParamsAppendAll } from '../lib/http.js' +import type { httpMethodGet, httpMethodPost } from '../lib/http.js' import { casesExhausted, isString, type MaybePromise } from '../lib/prelude.js' import { Transport } from '../types/Transport.js' +import type { TransportHttp, TransportMemory } from '../types/Transport.js' import { decodeResultData } from './CustomScalars/decode.js' import { encodeRequestVariables } from './CustomScalars/encode.js' -import type { FormattedExecutionResult, GraphQLSchema } from 'graphql' -import type { Context } from '../client/context.js' -import type { Config } from '../client/Settings/Config.js' -import type { MethodModePost } from '../client/transportHttp/request.js' -import type { httpMethodGet, httpMethodPost } from '../lib/http.js' -import type { TransportHttp, TransportMemory } from '../types/Transport.js' - export const requestPipeline = Anyware.Pipeline .createWithSpec({ steps: [{ @@ -192,6 +192,13 @@ export const requestPipeline = Anyware.Pipeline }) export namespace requestPipeline { + export type ResultFailure = Anyware.Pipeline.ResultFailure + // | Errors.ContextualError + // Possible from http transport fetch with abort controller. + // | DOMException + + export type Result<$Config extends Config = Config> = Anyware.Pipeline.InferResultFromSpec> + export type Spec<$Config extends Config = Config> = Anyware.PipelineSpecFromSteps<[ Steps.HookDefEncode<$Config>, Steps.HookDefPack<$Config>, @@ -291,7 +298,7 @@ export namespace requestPipeline { { response: Response } > & { result: FormattedExecutionResult } - output: 'todo' + output: GraffleExecutionResultEnvelope<$Config> } /** From cf6d67f5a2a375a5298d38d36d0921249a84554b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 12:49:08 -0500 Subject: [PATCH 21/36] clean --- src/client/handleOutput.ts | 39 -------------------------------------- 1 file changed, 39 deletions(-) diff --git a/src/client/handleOutput.ts b/src/client/handleOutput.ts index 31b2d9a71..5e68a218f 100644 --- a/src/client/handleOutput.ts +++ b/src/client/handleOutput.ts @@ -149,45 +149,6 @@ type HandleOutput_Envelope<$Context extends Context, $Envelope extends GraffleEx ? $Envelope : ExcludeUndefined<$Envelope['data']> // todo make data field not undefinable -// type HandleOutputGql_Envelope - -// | IfConfiguredGetOutputErrorReturns<$Config> -// | ( -// $Config['output']['envelope']['enabled'] extends true -// // todo even when envelope is enabled, its possible errors can not be included in its output. -// // When not, undefined should be removed from the data property. -// ? Envelope<$Config, $Data> -// // Note 1 -// // `undefined` is not a possible type because that would only happen if an error occurred. -// // If an error occurs when the envelope is disabled then either it throws or is returned. -// // No case swallows the error and returns undefined data. -// // -// // Note 2 -// // null is possible because of GraphQL null propagation. -// // todo We need to integrate this reality into the the other typed non-envelope output types too. -// : $Data | null -// ) - -// // dprint-ignore -// export type HandleOutputGraffleRootType<$Config extends Config, $Data> = -// | IfConfiguredGetOutputErrorReturns<$Config> -// | ( -// $Config['output']['envelope']['enabled'] extends true -// ? Envelope<$Config, $Data> -// : $Data -// ) - -// // dprint-ignore -// export type HandleOutputGraffleRootField<$Config extends Config, $RootFieldName extends string, $Data> = -// | IfConfiguredGetOutputErrorReturns<$Config> -// | ( -// $Config['output']['envelope']['enabled'] extends true -// // todo: a typed execution result that allows for additional error types. -// // currently it is always graphql execution error however envelope configuration can put more errors into that. -// ? Envelope<$Config, { [_ in $RootFieldName]: $Data }> -// : $Data -// ) - // dprint-ignore type IfConfiguredGetOutputErrorReturns<$Context extends Context> = | (ConfigGetOutputError<$Context, 'execution'> extends 'return' ? GraphQLExecutionResultError : never) From 5cdce43f41177b3134b2199a8f65edcd87ef3122 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 12:51:00 -0500 Subject: [PATCH 22/36] lint --- src/client/handleOutput.ts | 5 ----- src/lib/anyware/Pipeline/builder.ts | 1 - src/lib/anyware/Pipeline/run.ts | 2 +- src/requestPipeline/RequestPipeline.ts | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/client/handleOutput.ts b/src/client/handleOutput.ts index 5e68a218f..462ca48ad 100644 --- a/src/client/handleOutput.ts +++ b/src/client/handleOutput.ts @@ -24,7 +24,6 @@ import { } from './Settings/Config.js' export type GraffleExecutionResultEnvelope<$Config extends Config = Config> = - // & ExecutionResult & { errors?: ReadonlyArray< // formatted comes from http transport @@ -49,10 +48,6 @@ export type GraffleExecutionResultEnvelope<$Config extends Config = Config> = } : {}) -// export type GraffleExecutionResultVar<$Config extends Config = Config> = -// | GraffleExecutionResultEnvelope<$Config> -// | ErrorsOther - export const handleOutput = ( state: Context, result: requestPipeline.Result, diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 04b8799d0..90bab2ec9 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -3,7 +3,6 @@ import { type Tuple } from '../../prelude.js' import type { ExecutableStep } from '../ExecutableStep.js' import type { Step } from '../Step.js' import { type Config, type Options, resolveOptions } from './Config.js' -import type { Result } from './Result.js' export interface Context { config: Config diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts index 93d4eda19..6caf03923 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/Pipeline/run.ts @@ -20,7 +20,7 @@ type Run = < /** * todo */ -export const run: Run = (pipeline, params) => { +export const run: Run = (_pipeline, _params) => { // todo return undefined as any } diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index cb87bd129..9c2606e1b 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -67,7 +67,7 @@ export const requestPipeline = Anyware.Pipeline const methodMode = input.state.config.transport.config.methodMode const requestMethod = methodMode === MethodMode.post ? `post` - : methodMode === MethodMode.getReads + : methodMode === MethodMode.getReads // eslint-disable-line @typescript-eslint/no-unnecessary-condition ? OperationTypeToAccessKind[operationType] === `read` ? `get` : `post` : casesExhausted(methodMode) From 5d1f02e32f17e3771dea975a226063c743e17c27 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 13:05:47 -0500 Subject: [PATCH 23/36] merge executable and optimized --- .../anyware/Interceptor/Interceptor.test-d.ts | 4 ++-- src/lib/anyware/Pipeline/Executable.ts | 14 +++++++++++++ src/lib/anyware/Pipeline/_.ts | 1 + src/lib/anyware/Pipeline/__.ts | 20 ------------------- src/lib/anyware/Pipeline/builder.ts | 8 ++++++++ .../anyware/Pipeline/createWithSpec.test-d.ts | 4 +++- src/lib/anyware/Pipeline/createWithSpec.ts | 6 ++++-- src/lib/anyware/Pipeline/run.ts | 2 +- src/lib/anyware/run/OptimizedPipeline.ts | 14 ------------- src/lib/anyware/run/getEntrypoint.ts | 5 +++-- src/lib/anyware/run/runPipeline.ts | 4 ++-- src/lib/anyware/run/runStep.ts | 4 ++-- src/lib/anyware/run/runner.ts | 16 ++++++--------- 13 files changed, 46 insertions(+), 56 deletions(-) create mode 100644 src/lib/anyware/Pipeline/Executable.ts delete mode 100644 src/lib/anyware/run/OptimizedPipeline.ts diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 8ffe8baea..4d449389a 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -91,6 +91,6 @@ describe(`interceptor constructor`, () => { // dprint-ignore // @ts-expect-error -type GetTriggerFromPipeline<$Pipeline extends Pipeline, $TriggerName extends string> = Parameters>[0][$TriggerName] +type GetTriggerFromPipeline<$Pipeline extends Pipeline.PipelineExecutable, $TriggerName extends string> = Parameters>[0][$TriggerName] // dprint-ignore -type GetReturnTypeFromPipeline<$Pipeline extends Pipeline> = ReturnType> +type GetReturnTypeFromPipeline<$Pipeline extends Pipeline.PipelineExecutable> = ReturnType> diff --git a/src/lib/anyware/Pipeline/Executable.ts b/src/lib/anyware/Pipeline/Executable.ts new file mode 100644 index 000000000..e2766365f --- /dev/null +++ b/src/lib/anyware/Pipeline/Executable.ts @@ -0,0 +1,14 @@ +import type { ExecutableStep } from '../ExecutableStep.js' +import type { Step } from '../Step.js' +import type { Config } from './Config.js' +import type { PipelineSpec } from './Spec.js' + +// todo - spec as a property, not extended +// todo - output type NOT spec but final type +export interface PipelineExecutable extends PipelineSpec { + config: Config + steps: ExecutableStep[] + stepsIndex: StepsIndex +} + +export type StepsIndex = Map diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index 48b57fbf5..d6aa9a2d8 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,5 +1,6 @@ export * from './builder.js' export * from './createWithSpec.js' +export * from './Executable.js' export * from './Result.js' export * from './run.js' export * from './Spec.js' diff --git a/src/lib/anyware/Pipeline/__.ts b/src/lib/anyware/Pipeline/__.ts index 626718785..5ffe95bb1 100644 --- a/src/lib/anyware/Pipeline/__.ts +++ b/src/lib/anyware/Pipeline/__.ts @@ -1,21 +1 @@ -import type { ExecutableStep } from '../ExecutableStep.js' -import type { Config } from './Config.js' -import type { PipelineSpec } from './Spec.js' - export * as Pipeline from './_.js' - -// todo reconsider the division between these two types. -// Spec is fully declarative while this has "executable" steps -// which are spec steps with function signatures -// The problem: steps become executable but output remains in its declarative form -// One could also rename this type to "ExecutablePipeline" -// Where is this type _actually_ needed? The executable from seems to be a implementation detail? -// Can the external interface be just the spec type? -// The builder by definition includes the executable steps and infers the spec _from that_. So -// in that case its understandable. How about we introduce an "InferSpec" type + make executable pipeline -// have a spec property. Then use of utility types can go like: Anyware.Interceptor.Infer -// or make utility types accept spec OR executable and do the conditional branch of looking up ['spec'] within. -export interface Pipeline extends PipelineSpec { - config: Config - steps: ExecutableStep[] -} diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 90bab2ec9..0ef830544 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -3,6 +3,7 @@ import { type Tuple } from '../../prelude.js' import type { ExecutableStep } from '../ExecutableStep.js' import type { Step } from '../Step.js' import { type Config, type Options, resolveOptions } from './Config.js' +import type { StepsIndex } from './Executable.js' export interface Context { config: Config @@ -101,6 +102,7 @@ export type InferPipeline<$Builder extends Builder> = InferPipelineFromContext<$ type InferPipelineFromContext<$Context extends Context> = & $Context & { + stepsIndex: StepsIndex /** * The overall result of the pipeline. * @@ -130,3 +132,9 @@ export const create = <$Input extends object>(options?: Options): Builder<{ const _config = resolveOptions(options) return undefined as any } + +// todo: for the done method +// export const optimizePipeline = (pipeline: PipelineExecutable): OptimizedPipeline => { +// const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) +// return { ...pipeline, stepsIndex } +// } diff --git a/src/lib/anyware/Pipeline/createWithSpec.test-d.ts b/src/lib/anyware/Pipeline/createWithSpec.test-d.ts index c5217aac0..9f18c192e 100644 --- a/src/lib/anyware/Pipeline/createWithSpec.test-d.ts +++ b/src/lib/anyware/Pipeline/createWithSpec.test-d.ts @@ -2,12 +2,13 @@ import { assertEqual } from '../../assert-equal.js' import type { MaybePromise } from '../../prelude.js' import type { Config } from './Config.js' import { createWithSpec } from './createWithSpec.js' +import type { StepsIndex } from './Executable.js' import type { PipelineSpecFromSteps } from './Spec.js' { type Spec = PipelineSpecFromSteps<[]> const p = createWithSpec({ steps: [] }) - assertEqual() + assertEqual() } { @@ -23,6 +24,7 @@ import type { PipelineSpecFromSteps } from './Spec.js' steps: [ { run: (...arg: any[]) => any; name: `a`; input: object; output: MaybePromise<1> }, ] + stepsIndex: StepsIndex } >() } diff --git a/src/lib/anyware/Pipeline/createWithSpec.ts b/src/lib/anyware/Pipeline/createWithSpec.ts index e57805b8f..cb289f88c 100644 --- a/src/lib/anyware/Pipeline/createWithSpec.ts +++ b/src/lib/anyware/Pipeline/createWithSpec.ts @@ -2,6 +2,7 @@ import type { ConfigManager } from '../../config-manager/__.js' import type { Tuple } from '../../prelude.js' import type { Step } from '../Step.js' import { type Config, type Options, resolveOptions } from './Config.js' +import type { StepsIndex } from './Executable.js' import type { PipelineSpec } from './Spec.js' export const createWithSpec = <$PipelineSpec extends PipelineSpec>( @@ -9,16 +10,17 @@ export const createWithSpec = <$PipelineSpec extends PipelineSpec>( options?: Options steps: InferStepsInput<$PipelineSpec['steps']> }, -): InferPipeline<$PipelineSpec> => { +): InferPipelineExecutable<$PipelineSpec> => { const _config = resolveOptions(input.options) input return undefined as any } -type InferPipeline<$PipelineSpec extends PipelineSpec> = { +type InferPipelineExecutable<$PipelineSpec extends PipelineSpec> = { input: $PipelineSpec['input'] output: $PipelineSpec['output'] steps: InferExecutableSteps<$PipelineSpec['steps']> + stepsIndex: StepsIndex config: Config } diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts index 6caf03923..6f2acfb0d 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/Pipeline/run.ts @@ -8,7 +8,7 @@ interface Params { } type Run = < - $Pipeline extends Pipeline, + $Pipeline extends Pipeline.PipelineExecutable, $Params extends Params, >( pipeline: $Pipeline, diff --git a/src/lib/anyware/run/OptimizedPipeline.ts b/src/lib/anyware/run/OptimizedPipeline.ts deleted file mode 100644 index 1c818173d..000000000 --- a/src/lib/anyware/run/OptimizedPipeline.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ExecutableStep } from '../ExecutableStep.js' -import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Step.js' - -export type StepsIndex = Map - -export interface OptimizedPipeline extends Pipeline { - stepsIndex: StepsIndex -} - -export const optimizePipeline = (pipeline: Pipeline): OptimizedPipeline => { - const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) - return { ...pipeline, stepsIndex } -} diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index af9482c66..9dfd62bae 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,8 +1,8 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' +import type { PipelineExecutable } from '../Pipeline/Executable.js' import type { Step } from '../Step.js' -import type { StepsIndex } from './OptimizedPipeline.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< 'ErrorGraffleInterceptorEntryHook', @@ -25,9 +25,10 @@ export const InterceptorEntryHookIssue = { export type InterceptorEntryHookIssue = typeof InterceptorEntryHookIssue[keyof typeof InterceptorEntryHookIssue] export const getEntryStep = ( - stepsIndex: StepsIndex, + pipeline: PipelineExecutable, interceptor: NonRetryingInterceptorInput, ): ErrorAnywareInterceptorEntrypoint | Step => { + const stepsIndex = pipeline.stepsIndex const x = analyzeFunction(interceptor) if (x.parameters.length > 1) { return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.multipleParameters }) diff --git a/src/lib/anyware/run/runPipeline.ts b/src/lib/anyware/run/runPipeline.ts index b01e52183..7ed39208a 100644 --- a/src/lib/anyware/run/runPipeline.ts +++ b/src/lib/anyware/run/runPipeline.ts @@ -2,9 +2,9 @@ import type { Errors } from '../../errors/__.js' import { ContextualError } from '../../errors/ContextualError.js' import { casesExhausted, createDeferred, debug } from '../../prelude.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' +import type { PipelineExecutable } from '../Pipeline/Executable.js' import type { Step } from '../Step.js' import type { StepResult, StepResultErrorAsync } from '../StepResult.js' -import type { OptimizedPipeline } from './OptimizedPipeline.js' import { createResultEnvelope } from './resultEnvelope.js' import type { ResultEnvelop } from './resultEnvelope.js' import { runStep } from './runStep.js' @@ -20,7 +20,7 @@ export const runPipeline = async ( asyncErrorDeferred, previousStepsCompleted, }: { - pipeline: OptimizedPipeline + pipeline: PipelineExecutable stepsToProcess: readonly Step[] originalInputOrResult: unknown interceptorsStack: readonly InterceptorGeneric[] diff --git a/src/lib/anyware/run/runStep.ts b/src/lib/anyware/run/runStep.ts index d8a1c8f04..c9744d9f6 100644 --- a/src/lib/anyware/run/runStep.ts +++ b/src/lib/anyware/run/runStep.ts @@ -1,11 +1,11 @@ import { Errors } from '../../errors/__.js' import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../../prelude.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' +import type { PipelineExecutable } from '../Pipeline/Executable.js' import type { Step } from '../Step.js' import type { StepResult, StepResultErrorAsync } from '../StepResult.js' import { StepTrigger } from '../StepTrigger.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' -import type { OptimizedPipeline } from './OptimizedPipeline.js' import type { ResultEnvelop } from './resultEnvelope.js' type HookDoneResolver = (input: StepResult) => void @@ -27,7 +27,7 @@ export const runStep = async ( asyncErrorDeferred, customSlots, }: { - pipeline: OptimizedPipeline + pipeline: PipelineExecutable name: string done: HookDoneResolver inputOriginalOrFromExtension: object diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 1a6897a87..8d8edfc89 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -8,31 +8,27 @@ import type { Step } from '../Step.js' import type { StepResultErrorExtension } from '../StepResult.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import { getEntryStep } from './getEntrypoint.js' -import type { OptimizedPipeline } from './OptimizedPipeline.js' -import { optimizePipeline } from './OptimizedPipeline.js' import { runPipeline } from './runPipeline.js' export const createRunner = - <$Pipeline extends Pipeline>(pipeline: $Pipeline) => + <$Pipeline extends Pipeline.PipelineExecutable>(pipeline: $Pipeline) => async ({ initialInput, interceptors, retryingInterceptor }: { initialInput: $Pipeline['input'] // todo Pipeline needs to become sub-type of PipelineSpec then it should be accepted just fine. interceptors: Interceptor.InferConstructor<$Pipeline>[] retryingInterceptor?: Interceptor.InferConstructor<$Pipeline> }): Promise | Errors.ContextualError> => { - const optimizedPipeline = optimizePipeline(pipeline) + // const optimizedPipeline = optimizePipeline(pipeline) const interceptors_ = retryingInterceptor ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] : interceptors - const initialHookStackAndErrors = interceptors_.map(extension => - toInternalInterceptor(optimizedPipeline, extension) - ) + const initialHookStackAndErrors = interceptors_.map(extension => toInternalInterceptor(pipeline, extension)) const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) if (error) return error const asyncErrorDeferred = createDeferred({ strict: false }) const result = await runPipeline({ - pipeline: optimizedPipeline, + pipeline, stepsToProcess: pipeline.steps, originalInputOrResult: initialInput, // todo fix any @@ -45,7 +41,7 @@ export const createRunner = return result.result as any } -const toInternalInterceptor = (pipeline: OptimizedPipeline, interceptor: InterceptorInput) => { +const toInternalInterceptor = (pipeline: Pipeline.PipelineExecutable, interceptor: InterceptorInput) => { const currentChunk = createDeferred() const body = createDeferred() const extensionRun = typeof interceptor === `function` ? interceptor : interceptor.run @@ -73,7 +69,7 @@ const toInternalInterceptor = (pipeline: OptimizedPipeline, interceptor: Interce } case `optional`: case `required`: { - const entryStep = getEntryStep(pipeline.stepsIndex, extensionRun) + const entryStep = getEntryStep(pipeline, extensionRun) if (entryStep instanceof Error) { if (pipeline.config.entrypointSelectionMode === `required`) { return entryStep From 5a1305d75cee59045e8cb400f82c95b1e259405e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 22:38:15 -0500 Subject: [PATCH 24/36] begin fixing runtime --- src/lib/anyware/Pipeline/builder.ts | 34 ++++++++++++---- src/lib/anyware/Pipeline/run.ts | 7 ++-- src/lib/anyware/Step.ts | 4 +- src/lib/anyware/__.entrypoint.test.ts | 42 ++++++++++++++------ src/lib/anyware/run/getEntrypoint.ts | 6 +-- src/lib/anyware/run/runner.ts | 57 ++++++++++++++------------- 6 files changed, 93 insertions(+), 57 deletions(-) diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 0ef830544..0312f2e4e 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -129,12 +129,32 @@ export const create = <$Input extends object>(options?: Options): Builder<{ steps: [] config: Config }> => { - const _config = resolveOptions(options) - return undefined as any + const config = resolveOptions(options) + return recreate({ + steps: [], + config, + } as any) } -// todo: for the done method -// export const optimizePipeline = (pipeline: PipelineExecutable): OptimizedPipeline => { -// const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) -// return { ...pipeline, stepsIndex } -// } +const recreate = <$Context extends Context>(context: $Context): Builder<$Context> => { + return { + context, + step: (parameters) => { + return recreate({ + ...context, + steps: [ + ...context.steps, + parameters, + ], + } as any) + }, + done: () => { + const pipeline = context + const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) + return { + ...pipeline, + stepsIndex, + } as any + }, + } +} diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/Pipeline/run.ts index 6f2acfb0d..32bd129c5 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/Pipeline/run.ts @@ -1,3 +1,4 @@ +import { createRunner } from '../_.js' import type { Interceptor } from '../Interceptor/Interceptor.js' import type { Pipeline } from './__.js' import type { Result } from './Result.js' @@ -20,7 +21,7 @@ type Run = < /** * todo */ -export const run: Run = (_pipeline, _params) => { - // todo - return undefined as any +export const run: Run = async (pipeline, params) => { + const runner = createRunner(pipeline) + return await runner(params as any) as any } diff --git a/src/lib/anyware/Step.ts b/src/lib/anyware/Step.ts index 3d1253b57..235595428 100644 --- a/src/lib/anyware/Step.ts +++ b/src/lib/anyware/Step.ts @@ -40,9 +40,7 @@ export namespace Step { output: ReturnType<$Run> slots: undefined extends $Slots ? undefined : $Slots } => { - // todo - parameters - return undefined as any + return parameters as any } type ImplementationFn<$Input extends Input = Input> = (parameters: { input: $Input }) => any diff --git a/src/lib/anyware/__.entrypoint.test.ts b/src/lib/anyware/__.entrypoint.test.ts index 12d3a2982..a1c24ef1d 100644 --- a/src/lib/anyware/__.entrypoint.test.ts +++ b/src/lib/anyware/__.entrypoint.test.ts @@ -2,12 +2,12 @@ import { describe, expect, test } from 'vitest' import type { ContextualAggregateError } from '../errors/ContextualAggregateError.js' -import { _ } from '../prelude.js' -import { type Interceptor, Pipeline } from './_.js' -import { initialInput } from './__.test-helpers.js' +import { _, _ } from '../prelude.js' +import { Pipeline } from './_.js' +import { initialInput, stepA, stepB } from './__.test-helpers.js' const run = async (interceptor: (...args: any[]) => any) => { - const pipeline = Pipeline.create().done() + const pipeline = Pipeline.create().step(stepA).step(stepB).done() return Pipeline.run(pipeline, { initialInput, interceptors: [interceptor], @@ -27,16 +27,14 @@ describe(`invalid destructuring cases`, () => { "issue": "noParameters", }, "errors": [ - [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one entrypoint.], + [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one step.], ], "result": [ContextualAggregateError: One or more extensions are invalid.], } `) }) test(`destructuredWithoutEntryHook`, async () => { - const result = await run(async ({ x }) => { - return _ - }) as ContextualAggregateError + const result = await run(async ({}) => {}) as ContextualAggregateError expect({ result, errors: result.errors, @@ -45,10 +43,10 @@ describe(`invalid destructuring cases`, () => { ` { "context": { - "issue": "destructuredWithoutEntryHook", + "issue": "noParameters", }, "errors": [ - [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one entrypoint.], + [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one step.], ], "result": [ContextualAggregateError: One or more extensions are invalid.], } @@ -68,7 +66,7 @@ describe(`invalid destructuring cases`, () => { "issue": "multipleParameters", }, "errors": [ - [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one entrypoint.], + [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one step.], ], "result": [ContextualAggregateError: One or more extensions are invalid.], } @@ -87,7 +85,7 @@ describe(`invalid destructuring cases`, () => { "issue": "notDestructured", }, "errors": [ - [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one entrypoint.], + [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one step.], ], "result": [ContextualAggregateError: One or more extensions are invalid.], } @@ -105,7 +103,25 @@ describe(`invalid destructuring cases`, () => { "issue": "multipleDestructuredHookNames", }, "errors": [ - [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one entrypoint.], + [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one step.], + ], + "result": [ContextualAggregateError: One or more extensions are invalid.], + } + `) + }) + test(`invalidDestructuredHookNames`, async () => { + const result = await run(async ({ y, z }) => {}) as ContextualAggregateError + expect({ + result, + errors: result.errors, + context: result.errors[0]?.context, + }).toMatchInlineSnapshot(` + { + "context": { + "issue": "invalidDestructuredHookNames", + }, + "errors": [ + [ContextualError: Interceptor must destructure the first parameter passed to it and select exactly one step.], ], "result": [ContextualAggregateError: One or more extensions are invalid.], } diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index 9dfd62bae..0f24a3d28 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -10,7 +10,7 @@ export class ErrorAnywareInterceptorEntrypoint extends ContextualError< > { // todo add to context: parameters value parsed and raw constructor(context: { issue: InterceptorEntryHookIssue }) { - super(`Interceptor must destructure the first parameter passed to it and select exactly one entrypoint.`, context) + super(`Interceptor must destructure the first parameter passed to it and select exactly one step.`, context) } } @@ -20,6 +20,7 @@ export const InterceptorEntryHookIssue = { notDestructured: `notDestructured`, destructuredWithoutEntryHook: `destructuredWithoutEntryHook`, multipleDestructuredHookNames: `multipleDestructuredHookNames`, + invalidDestructuredHookNames: `invalidDestructuredHookNames`, } as const export type InterceptorEntryHookIssue = typeof InterceptorEntryHookIssue[keyof typeof InterceptorEntryHookIssue] @@ -51,8 +52,7 @@ export const getEntryStep = ( const stepName = steps[0] if (!stepName) { - // todo: destructured with invalid names - return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.multipleDestructuredHookNames }) + return new ErrorAnywareInterceptorEntrypoint({ issue: InterceptorEntryHookIssue.invalidDestructuredHookNames }) } const step = stepsIndex.get(stepName) diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 8d8edfc89..3f6212f81 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -10,36 +10,37 @@ import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import { getEntryStep } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' -export const createRunner = - <$Pipeline extends Pipeline.PipelineExecutable>(pipeline: $Pipeline) => - async ({ initialInput, interceptors, retryingInterceptor }: { - initialInput: $Pipeline['input'] - // todo Pipeline needs to become sub-type of PipelineSpec then it should be accepted just fine. - interceptors: Interceptor.InferConstructor<$Pipeline>[] - retryingInterceptor?: Interceptor.InferConstructor<$Pipeline> - }): Promise | Errors.ContextualError> => { - // const optimizedPipeline = optimizePipeline(pipeline) - const interceptors_ = retryingInterceptor - ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] - : interceptors - const initialHookStackAndErrors = interceptors_.map(extension => toInternalInterceptor(pipeline, extension)) - const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) - if (error) return error +export const createRunner = <$Pipeline extends Pipeline.PipelineExecutable>(pipeline: $Pipeline) => +async (params?: { + initialInput: $Pipeline['input'] + // todo Pipeline needs to become sub-type of PipelineSpec then it should be accepted just fine. + interceptors: Interceptor.InferConstructor<$Pipeline>[] + retryingInterceptor?: Interceptor.InferConstructor<$Pipeline> +}): Promise | Errors.ContextualError> => { + const { initialInput, interceptors = [], retryingInterceptor } = params ?? {} - const asyncErrorDeferred = createDeferred({ strict: false }) - const result = await runPipeline({ - pipeline, - stepsToProcess: pipeline.steps, - originalInputOrResult: initialInput, - // todo fix any - interceptorsStack: initialHookStack as any, - asyncErrorDeferred, - previousStepsCompleted: {}, - }) - if (result instanceof Error) return result + // const optimizedPipeline = optimizePipeline(pipeline) + const interceptors_ = retryingInterceptor + ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] + : interceptors + const initialHookStackAndErrors = interceptors_.map(extension => toInternalInterceptor(pipeline, extension)) + const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) + if (error) return error - return result.result as any - } + const asyncErrorDeferred = createDeferred({ strict: false }) + const result = await runPipeline({ + pipeline, + stepsToProcess: pipeline.steps, + originalInputOrResult: initialInput, + // todo fix any + interceptorsStack: initialHookStack as any, + asyncErrorDeferred, + previousStepsCompleted: {}, + }) + if (result instanceof Error) return result + + return result.result as any +} const toInternalInterceptor = (pipeline: Pipeline.PipelineExecutable, interceptor: InterceptorInput) => { const currentChunk = createDeferred() From 4f2e99ea54fc4f00e98d4d49e88bdc81d4ac5655 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 22:55:45 -0500 Subject: [PATCH 25/36] fix --- src/lib/anyware/__.entrypoint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/anyware/__.entrypoint.test.ts b/src/lib/anyware/__.entrypoint.test.ts index a1c24ef1d..9502c29c7 100644 --- a/src/lib/anyware/__.entrypoint.test.ts +++ b/src/lib/anyware/__.entrypoint.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest' import type { ContextualAggregateError } from '../errors/ContextualAggregateError.js' -import { _, _ } from '../prelude.js' +import { _ } from '../prelude.js' import { Pipeline } from './_.js' import { initialInput, stepA, stepB } from './__.test-helpers.js' From 4c70e0e9e8c9c62a1f17bbed29c6a4be50601989 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 9 Nov 2024 23:29:29 -0500 Subject: [PATCH 26/36] more passing tests --- src/lib/anyware/__.test-d.ts | 86 ------------------------------ src/lib/anyware/__.test-helpers.ts | 32 +++++++---- src/lib/anyware/__.test.ts | 48 +++++++++-------- 3 files changed, 49 insertions(+), 117 deletions(-) delete mode 100644 src/lib/anyware/__.test-d.ts diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts deleted file mode 100644 index 5c73f6a0e..000000000 --- a/src/lib/anyware/__.test-d.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable */ - -import { describe, expectTypeOf, test } from 'vitest' -import { assertEqual } from '../assert-equal.js' -import { ContextualError } from '../errors/ContextualError.js' -import { type MaybePromise } from '../prelude.js' -import { Anyware } from './__.js' -import type { StepTrigger } from './StepTrigger.js' - -// describe('without slots', () => { -// test('run', () => { -// type run = ReturnType['run'] - -// expectTypeOf().toMatchTypeOf< -// (input: { -// initialInput: InputA -// options?: Anyware.Options -// retryingExtension?: (input: { -// a: PublicHook< -// (params?: { input?: InputA }) => MaybePromise< -// Error | { -// b: PublicHook< -// (params?: { input?: InputB }) => MaybePromise -// > -// } -// > -// > -// b: PublicHook<(params?: { input?: InputB }) => MaybePromise> -// }) => Promise -// interceptors: ((input: { -// a: PublicHook< -// (params?: { input?: InputA }) => MaybePromise<{ -// b: PublicHook<(params?: { input?: InputB }) => MaybePromise> -// }> -// > -// b: PublicHook<(params?: { input?: InputB }) => MaybePromise> -// }) => Promise)[] -// }) => Promise -// >() -// }) -// }) - -// describe('withSlots', () => { -// const create = Anyware.create<['a'], { a: { input: InputA; slots: { x: (x: boolean) => number } } }, Result> - -// test('create', () => { -// expectTypeOf(create).toMatchTypeOf< -// (input: { -// hookNamesOrderedBySequence: ['a'] -// hooks: { -// a: { -// run: (input: { input: InputA; previous: {} }) => Result -// slots: { -// x: (x: boolean) => number -// } -// } -// } -// }) => any -// >() -// }) - -// test('run', () => { -// type run = ReturnType['run'] - -// expectTypeOf().toMatchTypeOf< -// (input: { -// initialInput: InputA -// options?: Anyware.Options -// interceptors: ((input: { -// a: PublicHook< -// ( -// input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }, -// ) => MaybePromise -// > -// }) => Promise)[] -// retryingExtension?: (input: { -// a: PublicHook< -// (input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }) => MaybePromise< -// Error | Result -// > -// > -// }) => Promise -// }) => Promise -// >() -// }) -// }) diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index a8e851a79..bad03de42 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -4,11 +4,14 @@ import type { Tuple } from '../prelude.js' import { Pipeline } from './_.js' import type { InterceptorInput } from './Interceptor/Interceptor.js' import type { Options } from './Pipeline/Config.js' +import type { PipelineExecutable } from './Pipeline/Executable.js' import { Step } from './Step.js' export const initialInput = { x: 1 } as const export type initialInput = typeof initialInput +export const initialInput2 = { value: `initial` } as const + export const results = { a: { a: 1 }, b: { b: 2 }, @@ -33,7 +36,8 @@ type PrivateHookRunnerInput = { } export const createPipeline = (options?: Options) => { - return Pipeline.create(options) + return Pipeline + .create<{ value: string }>(options) .step({ name: `a`, slots: { @@ -67,31 +71,39 @@ export const createPipeline = (options?: Options) => { .done() } -type TestBuilder = ReturnType +type TestPipeline = ReturnType -// @ts-expect-error -export let stepsIndex: Tuple.ToIndexByObjectKey = null +export let stepsIndex: Tuple.ToIndexByObjectKey +let pipeline: PipelineExecutable beforeEach(() => { - const pipeline = createPipeline() + pipeline = createPipeline() stepsIndex = keyBy(pipeline.steps, _ => _.name) as any }) export const runWithOptions = (options?: Options) => { - const builder = createPipeline(options) + const pipeline = createPipeline(options) const run = async (...interceptors: InterceptorInput[]) => { - return await Pipeline.run(builder, { - initialInput, + return await Pipeline.run(pipeline, { + initialInput: { value: `initial` }, // @ts-expect-error fixme interceptors, }) } + stepsIndex = keyBy(pipeline.steps, _ => _.name) as any return { - builder, + pipeline, + stepsIndex, run, } } -export const run = async (...extensions: InterceptorInput[]) => runWithOptions().run(...extensions) +export const run = async (...interceptors: InterceptorInput[]) => { + return await Pipeline.run(pipeline, { + initialInput: initialInput2, + // @ts-expect-error fixme + interceptors, + }) +} export const oops = new Error(`oops`) diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts index 89845c91a..c27ac8007 100644 --- a/src/lib/anyware/__.test.ts +++ b/src/lib/anyware/__.test.ts @@ -1,10 +1,10 @@ /* eslint-disable */ -import { describe, expect, test, vi } from 'vitest' +import { beforeEach, describe, expect, test, vi } from 'vitest' import { Errors } from '../errors/__.js' import type { ContextualError } from '../errors/ContextualError.js' import { Pipeline } from './_.js' -import { initialInput, oops, run, runWithOptions, stepsIndex } from './__.test-helpers.js' +import { initialInput2, oops, run, runWithOptions, stepsIndex } from './__.test-helpers.js' import { createRetryingInterceptor } from './Interceptor/Interceptor.js' import { Step } from './Step.js' @@ -95,13 +95,20 @@ describe(`one extension`, () => { }) }) -describe(`two extensions`, () => { - const { run } = runWithOptions({ entrypointSelectionMode: `optional` }) +describe(`two interceptors`, () => { + let run: ReturnType['run'] + let stepIndex: ReturnType['stepsIndex'] + + beforeEach(() => { + const info = runWithOptions({ entrypointSelectionMode: `optional` }) + run = info.run + stepIndex = info.stepsIndex + }) test(`first can short-circuit`, async () => { - const ex1 = () => 1 - const ex2 = vi.fn().mockImplementation(() => 2) - expect(await run(ex1, ex2)).toEqual(1) - expect(ex2).not.toHaveBeenCalled() + const i1 = () => 1 + const i2 = vi.fn().mockImplementation(() => 2) + expect(await run(i1, i2)).toEqual(1) + expect(i2).not.toHaveBeenCalled() expect(stepsIndex.a.run).not.toHaveBeenCalled() expect(stepsIndex.b.run).not.toHaveBeenCalled() }) @@ -134,32 +141,31 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+a+ex1+ex2+b` }) }) - test(`second can short-circuit before hook a`, async () => { + test(`second can short-circuit before step a`, async () => { let ex1AfterA = false - const ex1 = async ({ a }: any) => { + const i1 = async ({ a }: any) => { const { b } = await a({ value: a.input.value + `+ex1` }) ex1AfterA = true } - const ex2 = async ({ a }: any) => { - return 2 - } - expect(await run(ex1, ex2)).toEqual(2) + const i2 = async ({ a }: any) => 2 + + expect(await run(i1, i2)).toEqual(2) expect(ex1AfterA).toBe(false) expect(stepsIndex.a.run).not.toHaveBeenCalled() expect(stepsIndex.b.run).not.toHaveBeenCalled() }) - test(`second can short-circuit after hook a`, async () => { + test(`second can short-circuit after step a`, async () => { let ex1AfterB = false - const ex1 = async ({ a }: any) => { + const i1 = async ({ a }: any) => { const { b } = await a({ input: { value: a.input.value + `+ex1` } }) await b({ value: b.input.value + `+ex1` }) ex1AfterB = true } - const ex2 = async ({ a }: any) => { + const i2 = async ({ a }: any) => { await a({ value: a.input.value + `+ex2` }) return 2 } - expect(await run(ex1, ex2)).toEqual(2) + expect(await run(i1, i2)).toEqual(2) expect(ex1AfterB).toBe(false) expect(stepsIndex.a.run).toHaveBeenCalledOnce() expect(stepsIndex.b.run).not.toHaveBeenCalled() @@ -208,7 +214,7 @@ describe(`errors`, () => { `) }) - test(`if implementation fails, without extensions, result is the error`, async () => { + test(`if implementation fails, without interceptors, result is the error`, async () => { stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops) const result = await run() as ContextualError expect({ @@ -226,7 +232,7 @@ describe(`errors`, () => { } `) }) - test('calling a hook twice leads to clear error', async () => { + test('calling a step trigger twice leads to clear error', async () => { let neverRan = true const result = await run(async ({ a }) => { await a() @@ -393,7 +399,7 @@ describe('private hook parameter - previous', () => { return a() }) expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) - expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput } }) + expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput2 } }) }) test('contains the final input actually passed to the hook', async () => { From 7391da11e50a46b77b0ba1c9eff9f2bf67220cb3 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 13:46:29 -0500 Subject: [PATCH 27/36] retrying tidy --- .../anyware/Interceptor/Interceptor.test-d.ts | 3 +- src/lib/anyware/Interceptor/Interceptor.ts | 24 +++---- src/lib/anyware/Pipeline/_.ts | 2 +- src/lib/anyware/__.test-helpers.ts | 16 +++-- src/lib/anyware/__.test.ts | 52 +++++++-------- src/lib/anyware/{Pipeline => run}/run.ts | 11 +--- src/lib/anyware/run/runner.ts | 63 ++++++++++--------- 7 files changed, 89 insertions(+), 82 deletions(-) rename src/lib/anyware/{Pipeline => run}/run.ts (62%) diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 4d449389a..879f60ec9 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -1,6 +1,7 @@ import { describe, expectTypeOf, test } from 'vitest' import { _, type ExcludeUndefined } from '../../prelude.js' -import { type Interceptor, Pipeline } from '../_.js' +import type { Interceptor } from '../_.js' +import { Pipeline } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index ef60fa4db..c410ff3a2 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,6 +1,7 @@ import type { Simplify } from 'type-fest' import type { Deferred, MaybePromise } from '../../prelude.js' import type { PipelineSpec } from '../_.js' +import type { ResultSuccess } from '../Pipeline/Result.js' import type { Step } from '../Step.js' import type { StepTrigger } from '../StepTrigger.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' @@ -9,14 +10,6 @@ export type InterceptorOptions = { retrying: boolean } -// todo -export interface Interceptor { - name: string - // entrypoint: string - // body: Deferred - // currentChunk: Deferred -} - export namespace Interceptor { export interface InferConstructor< $PipelineSpec extends PipelineSpec = PipelineSpec, @@ -54,7 +47,7 @@ export type NonRetryingInterceptor = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } export type RetryingInterceptor = { @@ -62,16 +55,23 @@ export type RetryingInterceptor = { name: string entrypoint: string body: Deferred - currentChunk: Deferred + currentChunk: Deferred } -export const createRetryingInterceptor = (extension: NonRetryingInterceptorInput): RetryingInterceptorInput => { +export const createRetryingInterceptor = (interceptor: NonRetryingInterceptorInput): RetryingInterceptorInput => { return { retrying: true, - run: extension, + run: interceptor, } } +// export interface InterceptorInput { +// name: string +// // entrypoint: string +// // body: Deferred +// // currentChunk: Deferred +// } + // export type ExtensionInput<$Input extends object = object> = (input: $Input) => MaybePromise export type InterceptorInput<$Input extends object = any> = | NonRetryingInterceptorInput<$Input> diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts index d6aa9a2d8..93ccb23dc 100644 --- a/src/lib/anyware/Pipeline/_.ts +++ b/src/lib/anyware/Pipeline/_.ts @@ -1,6 +1,6 @@ +export * from '../run/run.js' export * from './builder.js' export * from './createWithSpec.js' export * from './Executable.js' export * from './Result.js' -export * from './run.js' export * from './Spec.js' diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index bad03de42..34f070d79 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -2,7 +2,7 @@ import { keyBy } from 'es-toolkit' import { beforeEach, vi } from 'vitest' import type { Tuple } from '../prelude.js' import { Pipeline } from './_.js' -import type { InterceptorInput } from './Interceptor/Interceptor.js' +import type { NonRetryingInterceptorInput } from './Interceptor/Interceptor.js' import type { Options } from './Pipeline/Config.js' import type { PipelineExecutable } from './Pipeline/Executable.js' import { Step } from './Step.js' @@ -83,10 +83,9 @@ beforeEach(() => { export const runWithOptions = (options?: Options) => { const pipeline = createPipeline(options) - const run = async (...interceptors: InterceptorInput[]) => { + const run = async (...interceptors: NonRetryingInterceptorInput[]) => { return await Pipeline.run(pipeline, { initialInput: { value: `initial` }, - // @ts-expect-error fixme interceptors, }) } @@ -98,12 +97,19 @@ export const runWithOptions = (options?: Options) => { } } -export const run = async (...interceptors: InterceptorInput[]) => { +export const run = async (...interceptors: NonRetryingInterceptorInput[]) => { return await Pipeline.run(pipeline, { initialInput: initialInput2, - // @ts-expect-error fixme interceptors, }) } +export const runRetrying = async (interceptor: NonRetryingInterceptorInput) => { + return await Pipeline.run(pipeline, { + initialInput: initialInput2, + interceptors: [], + retryingInterceptor: interceptor, + }) +} + export const oops = new Error(`oops`) diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts index c27ac8007..2711df6ce 100644 --- a/src/lib/anyware/__.test.ts +++ b/src/lib/anyware/__.test.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest' import { Errors } from '../errors/__.js' import type { ContextualError } from '../errors/ContextualError.js' import { Pipeline } from './_.js' -import { initialInput2, oops, run, runWithOptions, stepsIndex } from './__.test-helpers.js' +import { initialInput2, oops, run, runRetrying, runWithOptions, stepsIndex } from './__.test-helpers.js' import { createRetryingInterceptor } from './Interceptor/Interceptor.js' import { Step } from './Step.js' @@ -299,47 +299,47 @@ describe(`errors`, () => { describe('retrying extension', () => { test('if hook fails, extension can retry, then short-circuit', async () => { stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) - const result = await run(createRetryingInterceptor(async function foo({ a }) { + const result = await runRetrying(async function foo({ a }) { const result1 = await a() expect(result1).toEqual(oops) const result2 = await a() expect(typeof result2.b).toEqual('function') expect(result2.b.input).toEqual(1) return result2.b.input - })) + }) expect(result).toEqual(1) }) describe('errors', () => { - test('not last extension', async () => { - const result = await run( - createRetryingInterceptor(async function foo({ a }) { - return a() - }), - async function bar({ a }) { - return a() - }, - ) - expect(result).toMatchInlineSnapshot(`[ContextualError: Only the last extension can retry hooks.]`) - expect((result as Errors.ContextualError).context).toMatchInlineSnapshot(` - { - "extensionsAfter": [ - { - "name": "bar", - }, - ], - } - `) - }) + // test('not last extension', async () => { + // const result = await run( + // createRetryingInterceptor(async function foo({ a }) { + // return a() + // }), + // async function bar({ a }) { + // return a() + // }, + // ) + // expect(result).toMatchInlineSnapshot(`[ContextualError: Only the last extension can retry hooks.]`) + // expect((result as Errors.ContextualError).context).toMatchInlineSnapshot(` + // { + // "extensionsAfter": [ + // { + // "name": "bar", + // }, + // ], + // } + // `) + // }) test('call hook twice even though it succeeded the first time', async () => { let neverRan = true - const result = await run( - createRetryingInterceptor(async function foo({ a }) { + const result = await runRetrying( + async function foo({ a }) { const result1 = await a() expect('b' in result1).toBe(true) await a() // <-- Extension bug here under test. neverRan = false - }), + }, ) expect(neverRan).toBe(true) expect(result).toMatchInlineSnapshot( diff --git a/src/lib/anyware/Pipeline/run.ts b/src/lib/anyware/run/run.ts similarity index 62% rename from src/lib/anyware/Pipeline/run.ts rename to src/lib/anyware/run/run.ts index 32bd129c5..b0303710a 100644 --- a/src/lib/anyware/Pipeline/run.ts +++ b/src/lib/anyware/run/run.ts @@ -1,12 +1,7 @@ import { createRunner } from '../_.js' -import type { Interceptor } from '../Interceptor/Interceptor.js' -import type { Pipeline } from './__.js' -import type { Result } from './Result.js' - -interface Params { - initialInput: object - interceptors: Interceptor[] -} +import type { Pipeline } from '../Pipeline/__.js' +import type { Result } from '../Pipeline/Result.js' +import type { Params } from './runner.js' type Run = < $Pipeline extends Pipeline.PipelineExecutable, diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 3f6212f81..0d65eff84 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -2,7 +2,11 @@ import { partitionAndAggregateErrors } from '../../errors/_.js' import { Errors } from '../../errors/__.js' import { createDeferred } from '../../prelude.js' import { casesExhausted } from '../../prelude.js' -import { createRetryingInterceptor, type Interceptor, type InterceptorInput } from '../Interceptor/Interceptor.js' +import { + createRetryingInterceptor, + type InterceptorInput, + type NonRetryingInterceptorInput, +} from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' import type { Step } from '../Step.js' import type { StepResultErrorExtension } from '../StepResult.js' @@ -10,37 +14,38 @@ import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import { getEntryStep } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' -export const createRunner = <$Pipeline extends Pipeline.PipelineExecutable>(pipeline: $Pipeline) => -async (params?: { - initialInput: $Pipeline['input'] - // todo Pipeline needs to become sub-type of PipelineSpec then it should be accepted just fine. - interceptors: Interceptor.InferConstructor<$Pipeline>[] - retryingInterceptor?: Interceptor.InferConstructor<$Pipeline> -}): Promise | Errors.ContextualError> => { - const { initialInput, interceptors = [], retryingInterceptor } = params ?? {} +export interface Params { + initialInput: object + interceptors: NonRetryingInterceptorInput[] + retryingInterceptor?: NonRetryingInterceptorInput +} - // const optimizedPipeline = optimizePipeline(pipeline) - const interceptors_ = retryingInterceptor - ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] - : interceptors - const initialHookStackAndErrors = interceptors_.map(extension => toInternalInterceptor(pipeline, extension)) - const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) - if (error) return error +export const createRunner = + <$Pipeline extends Pipeline.PipelineExecutable>(pipeline: $Pipeline) => + async (params?: Params): Promise | Errors.ContextualError> => { + const { initialInput, interceptors = [], retryingInterceptor } = params ?? {} - const asyncErrorDeferred = createDeferred({ strict: false }) - const result = await runPipeline({ - pipeline, - stepsToProcess: pipeline.steps, - originalInputOrResult: initialInput, - // todo fix any - interceptorsStack: initialHookStack as any, - asyncErrorDeferred, - previousStepsCompleted: {}, - }) - if (result instanceof Error) return result + const interceptors_ = retryingInterceptor + ? [...interceptors, createRetryingInterceptor(retryingInterceptor)] + : interceptors + const initialHookStackAndErrors = interceptors_.map(extension => toInternalInterceptor(pipeline, extension)) + const [initialHookStack, error] = partitionAndAggregateErrors(initialHookStackAndErrors) + if (error) return error - return result.result as any -} + const asyncErrorDeferred = createDeferred({ strict: false }) + const result = await runPipeline({ + pipeline, + stepsToProcess: pipeline.steps, + originalInputOrResult: initialInput, + // todo fix any + interceptorsStack: initialHookStack as any, + asyncErrorDeferred, + previousStepsCompleted: {}, + }) + if (result instanceof Error) return result + + return result.result as any + } const toInternalInterceptor = (pipeline: Pipeline.PipelineExecutable, interceptor: InterceptorInput) => { const currentChunk = createDeferred() From d8cdbdf7220a6256689a39d0bf2b30215430e1b3 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:03:11 -0500 Subject: [PATCH 28/36] impl --- .../requestMethods/requestMethods.test.ts | 2 +- src/lib/anyware/ExecutableStep.ts | 3 +++ src/lib/anyware/Pipeline/Executable.ts | 4 ++-- src/lib/anyware/Pipeline/builder.ts | 3 ++- src/lib/anyware/Pipeline/createWithSpec.ts | 15 ++++++++++++--- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/documentBuilder/requestMethods/requestMethods.test.ts b/src/documentBuilder/requestMethods/requestMethods.test.ts index 328b88173..9a726dc70 100644 --- a/src/documentBuilder/requestMethods/requestMethods.test.ts +++ b/src/documentBuilder/requestMethods/requestMethods.test.ts @@ -3,7 +3,7 @@ import { DateScalar } from '../../../tests/_/fixtures/scalars.js' import { kitchenSink, test } from '../../../tests/_/helpers.js' describe(`query batch`, () => { - test(`success`, async ({ kitchenSinkData: db }) => { + test.only(`success`, async ({ kitchenSinkData: db }) => { expect(await kitchenSink.query.$batch({ id: true })).toMatchObject({ id: db.id }) }) test(`error`, async ({ kitchenSinkData: db }) => { diff --git a/src/lib/anyware/ExecutableStep.ts b/src/lib/anyware/ExecutableStep.ts index 3f4f6e1b1..7aa686402 100644 --- a/src/lib/anyware/ExecutableStep.ts +++ b/src/lib/anyware/ExecutableStep.ts @@ -3,3 +3,6 @@ import type { Step } from './Step.js' export interface ExecutableStep extends Step { run: (params: any) => any } + +export interface ExecutableStepRuntime extends Omit { +} diff --git a/src/lib/anyware/Pipeline/Executable.ts b/src/lib/anyware/Pipeline/Executable.ts index e2766365f..8e6245179 100644 --- a/src/lib/anyware/Pipeline/Executable.ts +++ b/src/lib/anyware/Pipeline/Executable.ts @@ -1,4 +1,4 @@ -import type { ExecutableStep } from '../ExecutableStep.js' +import type { ExecutableStep, ExecutableStepRuntime } from '../ExecutableStep.js' import type { Step } from '../Step.js' import type { Config } from './Config.js' import type { PipelineSpec } from './Spec.js' @@ -11,4 +11,4 @@ export interface PipelineExecutable extends PipelineSpec { stepsIndex: StepsIndex } -export type StepsIndex = Map +export type StepsIndex = Map diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts index 0312f2e4e..3671db89d 100644 --- a/src/lib/anyware/Pipeline/builder.ts +++ b/src/lib/anyware/Pipeline/builder.ts @@ -3,6 +3,7 @@ import { type Tuple } from '../../prelude.js' import type { ExecutableStep } from '../ExecutableStep.js' import type { Step } from '../Step.js' import { type Config, type Options, resolveOptions } from './Config.js' +import { createExecutableStepsIndex } from './createWithSpec.js' import type { StepsIndex } from './Executable.js' export interface Context { @@ -150,7 +151,7 @@ const recreate = <$Context extends Context>(context: $Context): Builder<$Context }, done: () => { const pipeline = context - const stepsIndex = new Map(pipeline.steps.map(step => [step.name, step])) + const stepsIndex = createExecutableStepsIndex(pipeline.steps) return { ...pipeline, stepsIndex, diff --git a/src/lib/anyware/Pipeline/createWithSpec.ts b/src/lib/anyware/Pipeline/createWithSpec.ts index cb289f88c..bff275f39 100644 --- a/src/lib/anyware/Pipeline/createWithSpec.ts +++ b/src/lib/anyware/Pipeline/createWithSpec.ts @@ -1,19 +1,28 @@ import type { ConfigManager } from '../../config-manager/__.js' import type { Tuple } from '../../prelude.js' +import type { ExecutableStepRuntime } from '../ExecutableStep.js' import type { Step } from '../Step.js' import { type Config, type Options, resolveOptions } from './Config.js' import type { StepsIndex } from './Executable.js' import type { PipelineSpec } from './Spec.js' +export const createExecutableStepsIndex = <$Steps extends ExecutableStepRuntime[]>(steps: $Steps): StepsIndex => { + return new Map(steps.map(step => [step.name, step])) +} + export const createWithSpec = <$PipelineSpec extends PipelineSpec>( input: { options?: Options steps: InferStepsInput<$PipelineSpec['steps']> }, ): InferPipelineExecutable<$PipelineSpec> => { - const _config = resolveOptions(input.options) - input - return undefined as any + const config = resolveOptions(input.options) + const stepsIndex = createExecutableStepsIndex(input.steps) + return { + config, + stepsIndex, + steps: input.steps, + } as any } type InferPipelineExecutable<$PipelineSpec extends PipelineSpec> = { From ca5f6fd52332746b006b827e88942b4eae6ce82b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:19:00 -0500 Subject: [PATCH 29/36] fix return success --- .../requestMethods/requestMethods.test.ts | 2 +- src/lib/anyware/ExecutableStep.ts | 3 +-- .../anyware/Interceptor/Interceptor.test-d.ts | 18 +++++++++--------- src/lib/anyware/run/run.test.ts | 15 +++++++++++++++ src/lib/anyware/run/run.ts | 2 +- src/lib/anyware/run/runner.ts | 12 +++++++----- 6 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 src/lib/anyware/run/run.test.ts diff --git a/src/documentBuilder/requestMethods/requestMethods.test.ts b/src/documentBuilder/requestMethods/requestMethods.test.ts index 9a726dc70..328b88173 100644 --- a/src/documentBuilder/requestMethods/requestMethods.test.ts +++ b/src/documentBuilder/requestMethods/requestMethods.test.ts @@ -3,7 +3,7 @@ import { DateScalar } from '../../../tests/_/fixtures/scalars.js' import { kitchenSink, test } from '../../../tests/_/helpers.js' describe(`query batch`, () => { - test.only(`success`, async ({ kitchenSinkData: db }) => { + test(`success`, async ({ kitchenSinkData: db }) => { expect(await kitchenSink.query.$batch({ id: true })).toMatchObject({ id: db.id }) }) test(`error`, async ({ kitchenSinkData: db }) => { diff --git a/src/lib/anyware/ExecutableStep.ts b/src/lib/anyware/ExecutableStep.ts index 7aa686402..b3aa5f91c 100644 --- a/src/lib/anyware/ExecutableStep.ts +++ b/src/lib/anyware/ExecutableStep.ts @@ -4,5 +4,4 @@ export interface ExecutableStep extends Step { run: (params: any) => any } -export interface ExecutableStepRuntime extends Omit { -} +export interface ExecutableStepRuntime extends Omit {} diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index 879f60ec9..da3b84d21 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -6,11 +6,11 @@ import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' -const p0 = Pipeline.create() +const b0 = Pipeline.create() describe(`interceptor constructor`, () => { test(`receives keyword arguments, a step trigger for each step`, () => { - const p1 = p0 + const p1 = b0 .step({ name: `a`, run: () => results.a }) .step({ name: `b`, run: () => results.b }) .step({ name: `c`, run: () => results.c }) @@ -27,13 +27,13 @@ describe(`interceptor constructor`, () => { // --- trigger --- test(`original input on self`, () => { - const p = p0.step({ name: `a`, run: () => results.a }).done() + const p = b0.step({ name: `a`, run: () => results.a }).done() type triggerA = GetTriggerFromPipeline expectTypeOf().toMatchTypeOf() }) test(`trigger arguments are optional`, () => { - const p = p0.step({ name: `a`, run: () => results.a }).done() + const p = b0.step({ name: `a`, run: () => results.a }).done() type triggerA = GetTriggerFromPipeline expectTypeOf<[]>().toMatchTypeOf>() }) @@ -41,7 +41,7 @@ describe(`interceptor constructor`, () => { // --- slots --- test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { - const p = p0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }).done() + const p = b0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }).done() type triggerA = GetTriggerFromPipeline type triggerB = GetTriggerFromPipeline expectTypeOf>().toEqualTypeOf<[params?: { @@ -55,14 +55,14 @@ describe(`interceptor constructor`, () => { }) test(`slots are optional`, () => { - const p = p0.step({ name: `a`, slots, run: () => results.a }).done() + const p = b0.step({ name: `a`, slots, run: () => results.a }).done() type triggerA = GetTriggerFromPipeline type triggerASlotInputs = ExcludeUndefined[0]>['using']> expectTypeOf<{ m?: any; n?: any }>().toMatchTypeOf() }) test(`slot function can return undefined (falls back to default slot)`, () => { - const p = p0.step({ name: `a`, slots, run: () => results.a }).done() + const p = b0.step({ name: `a`, slots, run: () => results.a }).done() type triggerA = GetTriggerFromPipeline type triggerASlotMOutput = ReturnType< ExcludeUndefined[0]>['using']>['m']> @@ -77,13 +77,13 @@ describe(`interceptor constructor`, () => { // --- output --- // test(`can return pipeline output or a step envelope`, () => { - const p = p0.step({ name: `a`, run: () => results.a }).done() + const p = b0.step({ name: `a`, run: () => results.a }).done() type i = GetReturnTypeFromPipeline expectTypeOf().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { - const p = p0.step({ name: `a`, run: () => Promise.resolve(results.a) }).done() + const p = b0.step({ name: `a`, run: () => Promise.resolve(results.a) }).done() expectTypeOf>().toEqualTypeOf>() }) }) diff --git a/src/lib/anyware/run/run.test.ts b/src/lib/anyware/run/run.test.ts new file mode 100644 index 000000000..14b6f3518 --- /dev/null +++ b/src/lib/anyware/run/run.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { Pipeline } from '../_.js' +import { type initialInput, results } from '../__.test-helpers.js' +import { run } from './run.js' + +const b0 = Pipeline.create() + +test(`interceptor returns raw value that gets wrapped into success type`, async () => { + const p = b0.step({ name: `a`, run: () => results.a }).done() + const r = await run(p, { + initialInput: { x: 1 }, + interceptors: [({ a }) => 1], + }) + expect(r).toEqual({ value: 1 }) +}) diff --git a/src/lib/anyware/run/run.ts b/src/lib/anyware/run/run.ts index b0303710a..b64742e65 100644 --- a/src/lib/anyware/run/run.ts +++ b/src/lib/anyware/run/run.ts @@ -5,7 +5,7 @@ import type { Params } from './runner.js' type Run = < $Pipeline extends Pipeline.PipelineExecutable, - $Params extends Params, + $Params extends Params<$Pipeline>, >( pipeline: $Pipeline, params?: $Params, diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 0d65eff84..3ae382b89 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -2,27 +2,29 @@ import { partitionAndAggregateErrors } from '../../errors/_.js' import { Errors } from '../../errors/__.js' import { createDeferred } from '../../prelude.js' import { casesExhausted } from '../../prelude.js' +import type { PipelineSpec } from '../_.js' import { createRetryingInterceptor, type InterceptorInput, type NonRetryingInterceptorInput, } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' +import type { InferResultFromSpec } from '../Pipeline/Result.js' import type { Step } from '../Step.js' import type { StepResultErrorExtension } from '../StepResult.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import { getEntryStep } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' -export interface Params { - initialInput: object +export interface Params<$Pipeline extends PipelineSpec = PipelineSpec> { + initialInput: $Pipeline['input'] interceptors: NonRetryingInterceptorInput[] retryingInterceptor?: NonRetryingInterceptorInput } export const createRunner = <$Pipeline extends Pipeline.PipelineExecutable>(pipeline: $Pipeline) => - async (params?: Params): Promise | Errors.ContextualError> => { + async (params?: Params<$Pipeline>): Promise> => { const { initialInput, interceptors = [], retryingInterceptor } = params ?? {} const interceptors_ = retryingInterceptor @@ -42,9 +44,9 @@ export const createRunner = asyncErrorDeferred, previousStepsCompleted: {}, }) - if (result instanceof Error) return result + if (result instanceof Error) return result as any - return result.result as any + return { value: result.result } as any } const toInternalInterceptor = (pipeline: Pipeline.PipelineExecutable, interceptor: InterceptorInput) => { From 01cbedc5174e6faaef6badc1103b9e619850a48e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:29:30 -0500 Subject: [PATCH 30/36] anyware tests --- src/lib/anyware/__.test.ts | 413 ------------------------------- src/lib/anyware/run/run.test.ts | 420 +++++++++++++++++++++++++++++++- 2 files changed, 410 insertions(+), 423 deletions(-) delete mode 100644 src/lib/anyware/__.test.ts diff --git a/src/lib/anyware/__.test.ts b/src/lib/anyware/__.test.ts deleted file mode 100644 index 2711df6ce..000000000 --- a/src/lib/anyware/__.test.ts +++ /dev/null @@ -1,413 +0,0 @@ -/* eslint-disable */ - -import { beforeEach, describe, expect, test, vi } from 'vitest' -import { Errors } from '../errors/__.js' -import type { ContextualError } from '../errors/ContextualError.js' -import { Pipeline } from './_.js' -import { initialInput2, oops, run, runRetrying, runWithOptions, stepsIndex } from './__.test-helpers.js' -import { createRetryingInterceptor } from './Interceptor/Interceptor.js' -import { Step } from './Step.js' - -describe(`no extensions`, () => { - test(`passthrough to implementation`, async () => { - const result = await run() - expect(result).toEqual({ value: `initial+a+b` }) - }) -}) - -describe(`one extension`, () => { - test(`can return own result`, async () => { - expect( - await run(async ({ a }) => { - const { b } = await a(a.input) - await b({ input: b.input }) - return 0 - }), - ).toEqual(0) - expect(stepsIndex.a.run.mock.calls[0]).toMatchObject([{ input: { value: `initial` } }]) - expect(stepsIndex.a.run).toHaveBeenCalled() - expect(stepsIndex.b.run).toHaveBeenCalled() - }) - test('can call hook with no input, making the original input be used', () => { - expect( - run(async ({ a }) => { - return await a() - }), - ).resolves.toEqual({ value: 'initial+a+b' }) - // todo why doesn't this work? - // expect(core.hooks.a).toHaveBeenCalled() - // expect(core.hooks.b).toHaveBeenCalled() - }) - describe(`can short-circuit`, () => { - test(`at start, return input`, async () => { - expect( - // todo arrow function expression parsing not working - await run(({ a }) => { - return a.input - }), - ).toEqual({ value: `initial` }) - expect(stepsIndex.a.run).not.toHaveBeenCalled() - expect(stepsIndex.b.run).not.toHaveBeenCalled() - }) - test(`at start, return own result`, async () => { - expect( - // todo arrow function expression parsing not working - await run(({ a }) => { - return 0 - }), - ).toEqual(0) - expect(stepsIndex.a.run).not.toHaveBeenCalled() - expect(stepsIndex.b.run).not.toHaveBeenCalled() - }) - test(`after first hook, return own result`, async () => { - expect( - await run(async ({ a }) => { - const { b } = await a({ input: a.input }) - return b.input.value + `+x` - }), - ).toEqual(`initial+a+x`) - expect(stepsIndex.b.run).not.toHaveBeenCalled() - }) - }) - describe(`can partially apply`, () => { - test(`only first hook`, async () => { - expect( - await run(async ({ a }) => { - return await a({ input: { value: a.input.value + `+ext` } }) - }), - ).toEqual({ value: `initial+ext+a+b` }) - }) - test(`only second hook`, async () => { - expect( - await run(async ({ b }) => { - return await b({ input: { value: b.input.value + `+ext` } }) - }), - ).toEqual({ value: `initial+a+ext+b` }) - }) - test(`only second hook + end`, async () => { - expect( - await run(async ({ b }) => { - const result = await b({ input: { value: b.input.value + `+ext` } }) - return result.value + `+end` - }), - ).toEqual(`initial+a+ext+b+end`) - }) - }) -}) - -describe(`two interceptors`, () => { - let run: ReturnType['run'] - let stepIndex: ReturnType['stepsIndex'] - - beforeEach(() => { - const info = runWithOptions({ entrypointSelectionMode: `optional` }) - run = info.run - stepIndex = info.stepsIndex - }) - test(`first can short-circuit`, async () => { - const i1 = () => 1 - const i2 = vi.fn().mockImplementation(() => 2) - expect(await run(i1, i2)).toEqual(1) - expect(i2).not.toHaveBeenCalled() - expect(stepsIndex.a.run).not.toHaveBeenCalled() - expect(stepsIndex.b.run).not.toHaveBeenCalled() - }) - - test(`each can adjust first hook then passthrough`, async () => { - const ex1 = ({ a }: any) => a({ input: { value: a.input.value + `+ex1` } }) - const ex2 = ({ a }: any) => a({ input: { value: a.input.value + `+ex2` } }) - expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+ex2+a+b` }) - }) - - test(`each can adjust each hook`, async () => { - const ex1 = async ({ a }: any) => { - const { b } = await a({ input: { value: a.input.value + `+ex1` } }) - return await b({ input: { value: b.input.value + `+ex1` } }) - } - const ex2 = async ({ a }: any) => { - const { b } = await a({ input: { value: a.input.value + `+ex2` } }) - return await b({ input: { value: b.input.value + `+ex2` } }) - } - expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+ex2+a+ex1+ex2+b` }) - }) - - test(`second can skip hook a`, async () => { - const ex1 = async ({ a }: any) => { - const { b } = await a({ input: { value: a.input.value + `+ex1` } }) - return await b({ input: { value: b.input.value + `+ex1` } }) - } - const ex2 = async ({ b }: any) => { - return await b({ input: { value: b.input.value + `+ex2` } }) - } - expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+a+ex1+ex2+b` }) - }) - test(`second can short-circuit before step a`, async () => { - let ex1AfterA = false - const i1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) - ex1AfterA = true - } - const i2 = async ({ a }: any) => 2 - - expect(await run(i1, i2)).toEqual(2) - expect(ex1AfterA).toBe(false) - expect(stepsIndex.a.run).not.toHaveBeenCalled() - expect(stepsIndex.b.run).not.toHaveBeenCalled() - }) - test(`second can short-circuit after step a`, async () => { - let ex1AfterB = false - const i1 = async ({ a }: any) => { - const { b } = await a({ input: { value: a.input.value + `+ex1` } }) - await b({ value: b.input.value + `+ex1` }) - ex1AfterB = true - } - const i2 = async ({ a }: any) => { - await a({ value: a.input.value + `+ex2` }) - return 2 - } - expect(await run(i1, i2)).toEqual(2) - expect(ex1AfterB).toBe(false) - expect(stepsIndex.a.run).toHaveBeenCalledOnce() - expect(stepsIndex.b.run).not.toHaveBeenCalled() - }) -}) - -describe(`errors`, () => { - test(`extension that throws a non-error is wrapped in error`, async () => { - const result = await run(async ({ a }) => { - throw `oops` - }) as ContextualError - expect({ - result, - context: result.context, - cause: result.cause, - }).toMatchInlineSnapshot(` - { - "cause": [Error: oops], - "context": { - "hookName": "a", - "interceptorName": "anonymous", - "source": "extension", - }, - "result": [ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "a".], - } - `) - }) - test(`extension throws asynchronously`, async () => { - const result = await run(async ({ a }) => { - throw oops - }) as ContextualError - expect({ - result, - context: result.context, - cause: result.cause, - }).toMatchInlineSnapshot(` - { - "cause": [Error: oops], - "context": { - "hookName": "a", - "interceptorName": "anonymous", - "source": "extension", - }, - "result": [ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "a".], - } - `) - }) - - test(`if implementation fails, without interceptors, result is the error`, async () => { - stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops) - const result = await run() as ContextualError - expect({ - result, - context: result.context, - cause: result.cause, - }).toMatchInlineSnapshot(` - { - "cause": [Error: oops], - "context": { - "hookName": "a", - "source": "implementation", - }, - "result": [ContextualError: There was an error in the core implementation of hook "a".], - } - `) - }) - test('calling a step trigger twice leads to clear error', async () => { - let neverRan = true - const result = await run(async ({ a }) => { - await a() - await a() - neverRan = false - }) as ContextualError - expect(neverRan).toBe(true) - const cause = result.cause as ContextualError - expect(cause.message).toMatchInlineSnapshot( - `"Only a retrying extension can retry hooks."`, - ) - expect(cause.context).toMatchInlineSnapshot(` - { - "extensionsAfter": [], - "hookName": "a", - } - `) - }) - describe.skip('certain errors can be configured to be re-thrown without wrapping error', () => { - class SpecialError1 extends Error {} - class SpecialError2 extends Error {} - const stepA = Step.createWithInput<{ throws: Error }>()({ - name: 'a', - run: ({ input }) => { - if (input.throws) throw input.throws - }, - }) - - test('via passthroughErrorInstanceOf (one)', async () => { - const builder = Pipeline.create<{ throws: Error }>({ - passthroughErrorInstanceOf: [SpecialError1], - }).step(stepA).done() - - // dprint-ignore - expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // dprint-ignore - expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) - }) - test('via passthroughErrorInstanceOf (multiple)', async () => { - const builder = Pipeline.create<{ throws: Error }>({ - passthroughErrorInstanceOf: [SpecialError1, SpecialError2], - }).step(stepA).done() - // dprint-ignore - expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // dprint-ignore - expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) - }) - test('via passthroughWith', async () => { - const builder = Pipeline.create<{ throws: Error }>({ - // todo type-safe hook name according to values passed to constructor - // todo type-tests on signal { hookName, source, error } - passthroughErrorWith: (signal) => { - return signal.error instanceof SpecialError1 - }, - }).step(stepA).done() - // dprint-ignore - expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) - // dprint-ignore - expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) - }) - }) -}) - -describe('retrying extension', () => { - test('if hook fails, extension can retry, then short-circuit', async () => { - stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) - const result = await runRetrying(async function foo({ a }) { - const result1 = await a() - expect(result1).toEqual(oops) - const result2 = await a() - expect(typeof result2.b).toEqual('function') - expect(result2.b.input).toEqual(1) - return result2.b.input - }) - expect(result).toEqual(1) - }) - - describe('errors', () => { - // test('not last extension', async () => { - // const result = await run( - // createRetryingInterceptor(async function foo({ a }) { - // return a() - // }), - // async function bar({ a }) { - // return a() - // }, - // ) - // expect(result).toMatchInlineSnapshot(`[ContextualError: Only the last extension can retry hooks.]`) - // expect((result as Errors.ContextualError).context).toMatchInlineSnapshot(` - // { - // "extensionsAfter": [ - // { - // "name": "bar", - // }, - // ], - // } - // `) - // }) - test('call hook twice even though it succeeded the first time', async () => { - let neverRan = true - const result = await runRetrying( - async function foo({ a }) { - const result1 = await a() - expect('b' in result1).toBe(true) - await a() // <-- Extension bug here under test. - neverRan = false - }, - ) - expect(neverRan).toBe(true) - expect(result).toMatchInlineSnapshot( - `[ContextualError: There was an error in the interceptor "foo".]`, - ) - expect((result as Errors.ContextualError).context).toMatchInlineSnapshot( - ` - { - "hookName": "a", - "interceptorName": "foo", - "source": "extension", - } - `, - ) - expect((result as Errors.ContextualError).cause).toMatchInlineSnapshot( - `[ContextualError: Only after failure can a hook be called again by a retrying extension.]`, - ) - }) - }) -}) - -describe('slots', () => { - test('have defaults that are called by default', async () => { - await run() - expect(stepsIndex.a.slots.append.mock.calls[0]).toMatchObject(['a']) - expect(stepsIndex.b.slots.append.mock.calls[0]).toMatchObject(['b']) - }) - test('extension can provide own function to slot on just one of a set of hooks', async () => { - const result = await run(async ({ a }) => { - return a({ using: { append: () => 'x' } }) - }) - expect(stepsIndex.a.slots.append).not.toBeCalled() - expect(stepsIndex.b.slots.append.mock.calls[0]).toMatchObject(['b']) - expect(result).toEqual({ value: 'initial+x+b' }) - }) - test('extension can provide own functions to slots on multiple of a set of hooks', async () => { - const result = await run(async ({ a }) => { - return a({ using: { append: () => 'x', appendExtra: () => '+x2' } }) - }) - expect(result).toEqual({ value: 'initial+x+x2+b' }) - }) - // todo hook with two slots - test('two extensions can each provide own function to same slot on just one of a set of hooks, and the later one wins', async () => { - const result = await run(async ({ a }) => { - const { b } = await a({ using: { append: () => 'x' } }) - return b({ using: { append: () => 'y' } }) - }) - expect(stepsIndex.a.slots.append).not.toBeCalled() - expect(stepsIndex.b.slots.append).not.toBeCalled() - expect(result).toEqual({ value: 'initial+x+y' }) - }) -}) - -describe('private hook parameter - previous', () => { - test('contains inputs of previous hooks', async () => { - await run(async ({ a }) => { - return a() - }) - expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) - expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput2 } }) - }) - - test('contains the final input actually passed to the hook', async () => { - const customInput = { value: 'custom' } - await run(async ({ a }) => { - return a({ input: customInput }) - }) - expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) - expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: customInput } }) - }) -}) diff --git a/src/lib/anyware/run/run.test.ts b/src/lib/anyware/run/run.test.ts index 14b6f3518..e07e0525c 100644 --- a/src/lib/anyware/run/run.test.ts +++ b/src/lib/anyware/run/run.test.ts @@ -1,15 +1,415 @@ -import { expect, test } from 'vitest' +/* eslint-disable */ + +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { Errors } from '../../errors/__.js' +import type { ContextualError } from '../../errors/ContextualError.js' import { Pipeline } from '../_.js' -import { type initialInput, results } from '../__.test-helpers.js' -import { run } from './run.js' +import { initialInput2, oops, run, runRetrying, runWithOptions, stepsIndex } from '../__.test-helpers.js' +import type { ResultSuccess } from '../Pipeline/Result.js' +import { Step } from '../Step.js' + +const successfulResult = <$Value>(value: $Value): ResultSuccess<$Value> => ({ value }) + +describe(`no interceptors`, () => { + test(`passthrough to implementation`, async () => { + const result = await run() + expect(result).toEqual({ value: { value: `initial+a+b` } }) + }) +}) + +describe(`one extension`, () => { + test(`can return own result`, async () => { + expect( + await run(async ({ a }) => { + const { b } = await a(a.input) + await b({ input: b.input }) + return 0 + }), + ).toEqual(successfulResult(0)) + expect(stepsIndex.a.run.mock.calls[0]).toMatchObject([{ input: { value: `initial` } }]) + expect(stepsIndex.a.run).toHaveBeenCalled() + expect(stepsIndex.b.run).toHaveBeenCalled() + }) + test('can call hook with no input, making the original input be used', () => { + expect( + run(async ({ a }) => { + return await a() + }), + ).resolves.toEqual({ value: { value: 'initial+a+b' } }) + // todo why doesn't this work? + // expect(core.hooks.a).toHaveBeenCalled() + // expect(core.hooks.b).toHaveBeenCalled() + }) + describe(`can short-circuit`, () => { + test(`at start, return input`, async () => { + expect( + // todo arrow function expression parsing not working + await run(({ a }) => { + return a.input + }), + ).toEqual({ value: { value: `initial` } }) + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() + }) + test(`at start, return own result`, async () => { + expect( + // todo arrow function expression parsing not working + await run(({ a }) => { + return 0 + }), + ).toEqual({ value: 0 }) + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() + }) + test(`after first hook, return own result`, async () => { + expect( + await run(async ({ a }) => { + const { b } = await a({ input: a.input }) + return b.input.value + `+x` + }), + ).toEqual(successfulResult(`initial+a+x`)) + expect(stepsIndex.b.run).not.toHaveBeenCalled() + }) + }) + describe(`can partially apply`, () => { + test(`only first hook`, async () => { + expect( + await run(async ({ a }) => { + return await a({ input: { value: a.input.value + `+ext` } }) + }), + ).toEqual(successfulResult({ value: `initial+ext+a+b` })) + }) + test(`only second hook`, async () => { + expect( + await run(async ({ b }) => { + return await b({ input: { value: b.input.value + `+ext` } }) + }), + ).toEqual(successfulResult({ value: `initial+a+ext+b` })) + }) + test(`only second hook + end`, async () => { + expect( + await run(async ({ b }) => { + const result = await b({ input: { value: b.input.value + `+ext` } }) + return result.value + `+end` + }), + ).toEqual(successfulResult(`initial+a+ext+b+end`)) + }) + }) +}) + +describe(`two interceptors`, () => { + let run: ReturnType['run'] + let stepIndex: ReturnType['stepsIndex'] -const b0 = Pipeline.create() + beforeEach(() => { + const info = runWithOptions({ entrypointSelectionMode: `optional` }) + run = info.run + stepIndex = info.stepsIndex + }) + test(`first can short-circuit`, async () => { + const i1 = () => 1 + const i2 = vi.fn().mockImplementation(() => 2) + expect(await run(i1, i2)).toEqual(successfulResult(1)) + expect(i2).not.toHaveBeenCalled() + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() + }) + + test(`each can adjust first hook then passthrough`, async () => { + const ex1 = ({ a }: any) => a({ input: { value: a.input.value + `+ex1` } }) + const ex2 = ({ a }: any) => a({ input: { value: a.input.value + `+ex2` } }) + expect(await run(ex1, ex2)).toEqual(successfulResult({ value: `initial+ex1+ex2+a+b` })) + }) + + test(`each can adjust each hook`, async () => { + const ex1 = async ({ a }: any) => { + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + return await b({ input: { value: b.input.value + `+ex1` } }) + } + const ex2 = async ({ a }: any) => { + const { b } = await a({ input: { value: a.input.value + `+ex2` } }) + return await b({ input: { value: b.input.value + `+ex2` } }) + } + expect(await run(ex1, ex2)).toEqual(successfulResult({ value: `initial+ex1+ex2+a+ex1+ex2+b` })) + }) + + test(`second can skip hook a`, async () => { + const ex1 = async ({ a }: any) => { + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + return await b({ input: { value: b.input.value + `+ex1` } }) + } + const ex2 = async ({ b }: any) => { + return await b({ input: { value: b.input.value + `+ex2` } }) + } + expect(await run(ex1, ex2)).toEqual(successfulResult({ value: `initial+ex1+a+ex1+ex2+b` })) + }) + test(`second can short-circuit before step a`, async () => { + let ex1AfterA = false + const i1 = async ({ a }: any) => { + const { b } = await a({ value: a.input.value + `+ex1` }) + ex1AfterA = true + } + const i2 = async ({ a }: any) => 2 + + expect(await run(i1, i2)).toEqual(successfulResult(2)) + expect(ex1AfterA).toBe(false) + expect(stepsIndex.a.run).not.toHaveBeenCalled() + expect(stepsIndex.b.run).not.toHaveBeenCalled() + }) + test(`second can short-circuit after step a`, async () => { + let ex1AfterB = false + const i1 = async ({ a }: any) => { + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + await b({ value: b.input.value + `+ex1` }) + ex1AfterB = true + } + const i2 = async ({ a }: any) => { + await a({ value: a.input.value + `+ex2` }) + return 2 + } + expect(await run(i1, i2)).toEqual(successfulResult(2)) + expect(ex1AfterB).toBe(false) + expect(stepsIndex.a.run).toHaveBeenCalledOnce() + expect(stepsIndex.b.run).not.toHaveBeenCalled() + }) +}) + +describe(`errors`, () => { + test(`extension that throws a non-error is wrapped in error`, async () => { + const result = await run(async ({ a }) => { + throw `oops` + }) as ContextualError + expect({ + result, + context: result.context, + cause: result.cause, + }).toMatchInlineSnapshot(` + { + "cause": [Error: oops], + "context": { + "hookName": "a", + "interceptorName": "anonymous", + "source": "extension", + }, + "result": [ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "a".], + } + `) + }) + test(`extension throws asynchronously`, async () => { + const result = await run(async ({ a }) => { + throw oops + }) as ContextualError + expect({ + result, + context: result.context, + cause: result.cause, + }).toMatchInlineSnapshot(` + { + "cause": [Error: oops], + "context": { + "hookName": "a", + "interceptorName": "anonymous", + "source": "extension", + }, + "result": [ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "a".], + } + `) + }) + + test(`if implementation fails, without interceptors, result is the error`, async () => { + stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops) + const result = await run() as ContextualError + expect({ + result, + context: result.context, + cause: result.cause, + }).toMatchInlineSnapshot(` + { + "cause": [Error: oops], + "context": { + "hookName": "a", + "source": "implementation", + }, + "result": [ContextualError: There was an error in the core implementation of hook "a".], + } + `) + }) + test('calling a step trigger twice leads to clear error', async () => { + let neverRan = true + const result = await run(async ({ a }) => { + await a() + await a() + neverRan = false + }) as ContextualError + expect(neverRan).toBe(true) + const cause = result.cause as ContextualError + expect(cause.message).toMatchInlineSnapshot( + `"Only a retrying extension can retry hooks."`, + ) + expect(cause.context).toMatchInlineSnapshot(` + { + "extensionsAfter": [], + "hookName": "a", + } + `) + }) + describe.skip('certain errors can be configured to be re-thrown without wrapping error', () => { + class SpecialError1 extends Error {} + class SpecialError2 extends Error {} + const stepA = Step.createWithInput<{ throws: Error }>()({ + name: 'a', + run: ({ input }) => { + if (input.throws) throw input.throws + }, + }) + + test('via passthroughErrorInstanceOf (one)', async () => { + const builder = Pipeline.create<{ throws: Error }>({ + passthroughErrorInstanceOf: [SpecialError1], + }).step(stepA).done() + + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + }) + test('via passthroughErrorInstanceOf (multiple)', async () => { + const builder = Pipeline.create<{ throws: Error }>({ + passthroughErrorInstanceOf: [SpecialError1, SpecialError2], + }).step(stepA).done() + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) + }) + test('via passthroughWith', async () => { + const builder = Pipeline.create<{ throws: Error }>({ + // todo type-safe hook name according to values passed to constructor + // todo type-tests on signal { hookName, source, error } + passthroughErrorWith: (signal) => { + return signal.error instanceof SpecialError1 + }, + }).step(stepA).done() + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + // dprint-ignore + expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + }) + }) +}) + +describe('retrying extension', () => { + test('if hook fails, extension can retry, then short-circuit', async () => { + stepsIndex.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) + const result = await runRetrying(async function foo({ a }) { + const result1 = await a() + expect(result1).toEqual(oops) + const result2 = await a() + expect(typeof result2.b).toEqual('function') + expect(result2.b.input).toEqual(1) + return result2.b.input + }) + expect(result).toEqual(successfulResult(1)) + }) + + describe('errors', () => { + // test('not last extension', async () => { + // const result = await run( + // createRetryingInterceptor(async function foo({ a }) { + // return a() + // }), + // async function bar({ a }) { + // return a() + // }, + // ) + // expect(result).toMatchInlineSnapshot(`[ContextualError: Only the last extension can retry hooks.]`) + // expect((result as Errors.ContextualError).context).toMatchInlineSnapshot(` + // { + // "extensionsAfter": [ + // { + // "name": "bar", + // }, + // ], + // } + // `) + // }) + test('call hook twice even though it succeeded the first time', async () => { + let neverRan = true + const result = await runRetrying( + async function foo({ a }) { + const result1 = await a() + expect('b' in result1).toBe(true) + await a() // <-- Extension bug here under test. + neverRan = false + }, + ) + expect(neverRan).toBe(true) + expect(result).toMatchInlineSnapshot( + `[ContextualError: There was an error in the interceptor "foo".]`, + ) + expect((result as Errors.ContextualError).context).toMatchInlineSnapshot( + ` + { + "hookName": "a", + "interceptorName": "foo", + "source": "extension", + } + `, + ) + expect((result as Errors.ContextualError).cause).toMatchInlineSnapshot( + `[ContextualError: Only after failure can a hook be called again by a retrying extension.]`, + ) + }) + }) +}) + +describe('slots', () => { + test('have defaults that are called by default', async () => { + await run() + expect(stepsIndex.a.slots.append.mock.calls[0]).toMatchObject(['a']) + expect(stepsIndex.b.slots.append.mock.calls[0]).toMatchObject(['b']) + }) + test('extension can provide own function to slot on just one of a set of hooks', async () => { + const result = await run(async ({ a }) => { + return a({ using: { append: () => 'x' } }) + }) + expect(stepsIndex.a.slots.append).not.toBeCalled() + expect(stepsIndex.b.slots.append.mock.calls[0]).toMatchObject(['b']) + expect(result).toEqual(successfulResult({ value: 'initial+x+b' })) + }) + test('extension can provide own functions to slots on multiple of a set of hooks', async () => { + const result = await run(async ({ a }) => { + return a({ using: { append: () => 'x', appendExtra: () => '+x2' } }) + }) + expect(result).toEqual(successfulResult({ value: 'initial+x+x2+b' })) + }) + // todo hook with two slots + test('two extensions can each provide own function to same slot on just one of a set of hooks, and the later one wins', async () => { + const result = await run(async ({ a }) => { + const { b } = await a({ using: { append: () => 'x' } }) + return b({ using: { append: () => 'y' } }) + }) + expect(stepsIndex.a.slots.append).not.toBeCalled() + expect(stepsIndex.b.slots.append).not.toBeCalled() + expect(result).toEqual(successfulResult({ value: 'initial+x+y' })) + }) +}) + +describe('private hook parameter - previous', () => { + test('contains inputs of previous hooks', async () => { + await run(async ({ a }) => { + return a() + }) + expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) + expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: initialInput2 } }) + }) -test(`interceptor returns raw value that gets wrapped into success type`, async () => { - const p = b0.step({ name: `a`, run: () => results.a }).done() - const r = await run(p, { - initialInput: { x: 1 }, - interceptors: [({ a }) => 1], + test('contains the final input actually passed to the hook', async () => { + const customInput = { value: 'custom' } + await run(async ({ a }) => { + return a({ input: customInput }) + }) + expect(stepsIndex.a.run.mock.calls[0]?.[0].previous).toEqual({}) + expect(stepsIndex.b.run.mock.calls[0]?.[0].previous).toEqual({ a: { input: customInput } }) }) - expect(r).toEqual({ value: 1 }) }) From 7cb4b49b514a857751c68a61daf44b1beec8da3c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:30:25 -0500 Subject: [PATCH 31/36] refactor --- src/lib/anyware/Pipeline/Result.ts | 2 ++ src/lib/anyware/run/run.test.ts | 4 +--- src/lib/anyware/run/runner.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/anyware/Pipeline/Result.ts b/src/lib/anyware/Pipeline/Result.ts index 1019ea209..6f56f5f8e 100644 --- a/src/lib/anyware/Pipeline/Result.ts +++ b/src/lib/anyware/Pipeline/Result.ts @@ -10,3 +10,5 @@ export type Result = ResultFailure | ResultSuccess export interface ResultSuccess { value: T } + +export const successfulResult = <$Value>(value: $Value): ResultSuccess<$Value> => ({ value }) diff --git a/src/lib/anyware/run/run.test.ts b/src/lib/anyware/run/run.test.ts index e07e0525c..18e611bdb 100644 --- a/src/lib/anyware/run/run.test.ts +++ b/src/lib/anyware/run/run.test.ts @@ -5,11 +5,9 @@ import { Errors } from '../../errors/__.js' import type { ContextualError } from '../../errors/ContextualError.js' import { Pipeline } from '../_.js' import { initialInput2, oops, run, runRetrying, runWithOptions, stepsIndex } from '../__.test-helpers.js' -import type { ResultSuccess } from '../Pipeline/Result.js' +import { type ResultSuccess, successfulResult } from '../Pipeline/Result.js' import { Step } from '../Step.js' -const successfulResult = <$Value>(value: $Value): ResultSuccess<$Value> => ({ value }) - describe(`no interceptors`, () => { test(`passthrough to implementation`, async () => { const result = await run() diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index 3ae382b89..d241bad3a 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -9,7 +9,7 @@ import { type NonRetryingInterceptorInput, } from '../Interceptor/Interceptor.js' import type { Pipeline } from '../Pipeline/__.js' -import type { InferResultFromSpec } from '../Pipeline/Result.js' +import { type InferResultFromSpec, successfulResult } from '../Pipeline/Result.js' import type { Step } from '../Step.js' import type { StepResultErrorExtension } from '../StepResult.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' @@ -46,7 +46,7 @@ export const createRunner = }) if (result instanceof Error) return result as any - return { value: result.result } as any + return successfulResult(result.result) } const toInternalInterceptor = (pipeline: Pipeline.PipelineExecutable, interceptor: InterceptorInput) => { From 1a1ca0cb3aa09bea831527a0c86260256168dd1b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:35:56 -0500 Subject: [PATCH 32/36] fix --- src/client/handleOutput.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/handleOutput.ts b/src/client/handleOutput.ts index 462ca48ad..076f1248d 100644 --- a/src/client/handleOutput.ts +++ b/src/client/handleOutput.ts @@ -54,7 +54,7 @@ export const handleOutput = ( ) => { if (isContextConfigTraditionalGraphQLOutput(state.config)) { if (result instanceof Error) throw result - return result + return result.value } const config = state.config From a05c43a2ee15619f5cb5e7189aa0d8b42e06e03d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:49:00 -0500 Subject: [PATCH 33/36] fixes --- src/lib/anyware/run/run.test.ts | 2 +- src/requestPipeline/RequestPipeline.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lib/anyware/run/run.test.ts b/src/lib/anyware/run/run.test.ts index 18e611bdb..9681c38b1 100644 --- a/src/lib/anyware/run/run.test.ts +++ b/src/lib/anyware/run/run.test.ts @@ -251,7 +251,7 @@ describe(`errors`, () => { } `) }) - describe.skip('certain errors can be configured to be re-thrown without wrapping error', () => { + describe('certain errors can be configured to be re-thrown without wrapping error', () => { class SpecialError1 extends Error {} class SpecialError2 extends Error {} const stepA = Step.createWithInput<{ throws: Error }>()({ diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index 9c2606e1b..9a7bd4532 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -13,7 +13,7 @@ import { getRequestHeadersRec, parseExecutionResult, postRequestHeadersRec } fro import { normalizeRequestToNode } from '../lib/grafaid/request.js' import { mergeRequestInit, searchParamsAppendAll } from '../lib/http.js' import type { httpMethodGet, httpMethodPost } from '../lib/http.js' -import { casesExhausted, isString, type MaybePromise } from '../lib/prelude.js' +import { casesExhausted, isAbortError, isString, type MaybePromise } from '../lib/prelude.js' import { Transport } from '../types/Transport.js' import type { TransportHttp, TransportMemory } from '../types/Transport.js' import { decodeResultData } from './CustomScalars/decode.js' @@ -21,6 +21,16 @@ import { encodeRequestVariables } from './CustomScalars/encode.js' export const requestPipeline = Anyware.Pipeline .createWithSpec({ + options: { + // If core errors caused by an abort error then raise it as a direct error. + // This is an expected possible error. Possible when user cancels a request. + passthroughErrorWith: (signal) => { + // todo have anyware propagate the input that was passed to the hook that failed. + // it will give us a bit more confidence that we're only allowing this abort error for fetch requests stuff + // context.config.transport.type === Transport.http + return signal.hookName === `exchange` && isAbortError(signal.error) + }, + }, steps: [{ name: `encode`, run: ({ input }): requestPipeline.Steps.HookDefPack['input'] => { From 4fe98fb893954e31e0c27d7b10f274a27fed9171 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:50:08 -0500 Subject: [PATCH 34/36] update stacks --- ...envelope_envelope-error__envelope-error.output.txt | 3 ++- ...elope_error-throw__envelope-error-throw.output.txt | 3 ++- .../output_preset__standard-graphql.output.txt | 11 ++++++----- .../20_output/output_return-error.output.txt | 3 ++- ...error-execution__return-error-execution.output.txt | 5 +++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/__outputs__/20_output/output_envelope_envelope-error__envelope-error.output.txt b/examples/__outputs__/20_output/output_envelope_envelope-error__envelope-error.output.txt index 2983beaac..16807a090 100644 --- a/examples/__outputs__/20_output/output_envelope_envelope-error__envelope-error.output.txt +++ b/examples/__outputs__/20_output/output_envelope_envelope-error__envelope-error.output.txt @@ -3,7 +3,8 @@ errors: [ ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "encode". at runPipeline (/some/path/to/runPipeline.ts:XX:XX:18) - at async Object.run (/some/path/to/runner.ts:XX:XX:20) + at async (/some/path/to/runner.ts:XX:XX:20) + at async Module.run (/some/path/to/run.ts:XX:XX:10) at async executeDocument (/some/path/to/requestMethods.ts:XX:XX:18) at async executeRootField (/some/path/to/requestMethods.ts:XX:XX:18) at async (/some/path/to/output_envelope_envelope-error__envelope-error.ts:XX:XX:16) { diff --git a/examples/__outputs__/20_output/output_envelope_envelope_error-throw__envelope-error-throw.output.txt b/examples/__outputs__/20_output/output_envelope_envelope_error-throw__envelope-error-throw.output.txt index 63ca338aa..a3ff9bbda 100644 --- a/examples/__outputs__/20_output/output_envelope_envelope_error-throw__envelope-error-throw.output.txt +++ b/examples/__outputs__/20_output/output_envelope_envelope_error-throw__envelope-error-throw.output.txt @@ -5,7 +5,8 @@ ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "encode". at runPipeline (/some/path/to/runPipeline.ts:XX:XX:18) - at async Object.run (/some/path/to/runner.ts:XX:XX:20) + at async (/some/path/to/runner.ts:XX:XX:20) + at async Module.run (/some/path/to/run.ts:XX:XX:10) at async executeDocument (/some/path/to/requestMethods.ts:XX:XX:18) at async executeRootField (/some/path/to/requestMethods.ts:XX:XX:18) at async (/some/path/to/output_envelope_envelope_error-throw__envelope-error-throw.ts:XX:XX:1) { diff --git a/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt b/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt index 245be6938..e0d39046f 100644 --- a/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt +++ b/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt @@ -7,25 +7,26 @@ ContextualError: There was an error in the core implementation of hook "exchange at runPipeline (/some/path/to/runPipeline.ts:XX:XX:18) at async runPipeline (/some/path/to/runPipeline.ts:XX:XX:14) at async runPipeline (/some/path/to/runPipeline.ts:XX:XX:14) - ... 2 lines matching cause stack trace ... + ... 3 lines matching cause stack trace ... at async (/some/path/to/output_preset__standard-graphql.ts:XX:XX:16) { context: { hookName: 'exchange', source: 'implementation' }, [cause]: TypeError: Failed to parse URL from ... at new Request (node:internal/deps/undici/undici:XX:XX) at Object.run (/some/path/to/RequestPipeline.ts:XX:XX:29) ... 6 lines matching cause stack trace ... + at async Object.send (/some/path/to/gql.ts:XX:XX:26) at async (/some/path/to/output_preset__standard-graphql.ts:XX:XX:16) { [cause]: TypeError: Invalid URL at new URL (node:internal/url:XX:XX) at new Request (node:internal/deps/undici/undici:XX:XX) at Object.run (/some/path/to/RequestPipeline.ts:XX:XX:29) - at runHook (/some/path/to/runHook.ts:XX:XX:37) + at runStep (/some/path/to/runStep.ts:XX:XX:37) at runPipeline (/some/path/to/runPipeline.ts:XX:XX:8) at runPipeline (/some/path/to/runPipeline.ts:XX:XX:20) at async runPipeline (/some/path/to/runPipeline.ts:XX:XX:14) - at async Object.run (/some/path/to/runner.ts:XX:XX:20) - at async Object.send (/some/path/to/gql.ts:XX:XX:26) - at async (/some/path/to/output_preset__standard-graphql.ts:XX:XX:16) { + at async (/some/path/to/runner.ts:XX:XX:20) + at async Module.run (/some/path/to/run.ts:XX:XX:10) + at async Object.send (/some/path/to/gql.ts:XX:XX:26) { code: 'ERR_INVALID_URL', input: '...' } diff --git a/examples/__outputs__/20_output/output_return-error.output.txt b/examples/__outputs__/20_output/output_return-error.output.txt index fc41848ac..0cbd2b8cf 100644 --- a/examples/__outputs__/20_output/output_return-error.output.txt +++ b/examples/__outputs__/20_output/output_return-error.output.txt @@ -1,7 +1,8 @@ ---------------------------------------- SHOW ---------------------------------------- ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "encode". at runPipeline (/some/path/to/runPipeline.ts:XX:XX:18) - at async Object.run (/some/path/to/runner.ts:XX:XX:20) + at async (/some/path/to/runner.ts:XX:XX:20) + at async Module.run (/some/path/to/run.ts:XX:XX:10) at async executeDocument (/some/path/to/requestMethods.ts:XX:XX:18) at async executeRootField (/some/path/to/requestMethods.ts:XX:XX:18) at async (/some/path/to/output_return-error.ts:XX:XX:18) { diff --git a/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt b/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt index 681338192..b46d3a402 100644 --- a/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt +++ b/examples/__outputs__/20_output/output_return-error_return-error-execution__return-error-execution.output.txt @@ -23,7 +23,7 @@ ContextualAggregateError: One or more errors in the execution result. ] at (/some/path/to/handleOutput.ts:XX:XX:16) at Array.map () - at handleOutput (/some/path/to/handleOutput.ts:XX:XX:21) + at handleOutput (/some/path/to/handleOutput.ts:XX:XX:27) at executeDocument (/some/path/to/requestMethods.ts:XX:XX:10) at process.processTicksAndRejections (node:internal/process/task_queues:XX:XX) at async executeRootField (/some/path/to/requestMethods.ts:XX:XX:18) @@ -37,7 +37,8 @@ ContextualAggregateError: One or more errors in the execution result. ContextualError: There was an error in the interceptor "anonymous" (use named functions to improve this error message) while running hook "encode". at runPipeline (/some/path/to/runPipeline.ts:XX:XX:18) at process.processTicksAndRejections (node:internal/process/task_queues:XX:XX) - at async Object.run (/some/path/to/runner.ts:XX:XX:20) + at async (/some/path/to/runner.ts:XX:XX:20) + at async Module.run (/some/path/to/run.ts:XX:XX:10) at async executeDocument (/some/path/to/requestMethods.ts:XX:XX:18) at async executeRootField (/some/path/to/requestMethods.ts:XX:XX:18) at async (/some/path/to/output_return-error_return-error-execution__return-error-execution.ts:XX:XX:3) { From 3a29791ff4f68a61d251b74de0533774a6d2d6f2 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:54:44 -0500 Subject: [PATCH 35/36] fix build --- src/lib/anyware/run/getEntrypoint.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index 0f24a3d28..b91c9fd4a 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,5 +1,6 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' +import type { ExecutableStep, ExecutableStepRuntime } from '../ExecutableStep.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' import type { PipelineExecutable } from '../Pipeline/Executable.js' import type { Step } from '../Step.js' @@ -28,7 +29,7 @@ export type InterceptorEntryHookIssue = typeof InterceptorEntryHookIssue[keyof t export const getEntryStep = ( pipeline: PipelineExecutable, interceptor: NonRetryingInterceptorInput, -): ErrorAnywareInterceptorEntrypoint | Step => { +): ErrorAnywareInterceptorEntrypoint | ExecutableStepRuntime => { const stepsIndex = pipeline.stepsIndex const x = analyzeFunction(interceptor) if (x.parameters.length > 1) { From 7d191e2367059ff88c45710e49967aededd1c737 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 11 Nov 2024 14:58:52 -0500 Subject: [PATCH 36/36] lint --- src/lib/anyware/run/getEntrypoint.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index b91c9fd4a..54fde57b5 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,9 +1,8 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' -import type { ExecutableStep, ExecutableStepRuntime } from '../ExecutableStep.js' +import type { ExecutableStepRuntime } from '../ExecutableStep.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' import type { PipelineExecutable } from '../Pipeline/Executable.js' -import type { Step } from '../Step.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< 'ErrorGraffleInterceptorEntryHook',