From 4d6e805efce5235a64622c4edfe47ee9bebd74f3 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 1 Apr 2026 14:13:00 +0300 Subject: [PATCH 1/2] Create typed generateId and typesafe dep accessor --- lib/block.ts | 22 +++++++++------ lib/context.ts | 11 ++++---- lib/depsDomain.ts | 55 ++++++++++++++++++++++++++++-------- lib/functionBlock.ts | 6 ++-- lib/index.ts | 2 ++ lib/types.ts | 13 ++++++--- tests/httpBlock.test.ts | 4 +-- tests/options.deps.test.ts | 50 +++++++++++++------------------- tests/options.params.test.ts | 4 +-- 9 files changed, 101 insertions(+), 66 deletions(-) diff --git a/lib/block.ts b/lib/block.ts index f2dd2b5..90982a8 100644 --- a/lib/block.ts +++ b/lib/block.ts @@ -1,7 +1,7 @@ import { createError, ERROR_ID } from './error'; -import type { DescriptBlockDeps, DescriptBlockId } from './depsDomain'; -import DepsDomain from './depsDomain'; +import type { DescriptBlockDeps, DescriptBlockId, UntypedId } from './depsDomain'; +import DepsDomain, { createDepAccessor } from './depsDomain'; import type Cancel from './cancel'; import type ContextClass from './context'; import type { BlockResultOut, InferResultOrResult, DescriptBlockOptions, DepsIds } from './types'; @@ -42,6 +42,8 @@ abstract class BaseBlock< ErrorResultOut = unknown, Params = ParamsOut, > { + declare readonly __resultType: InferResultOrResult; + protected block: CustomBlock; protected options: BlockOptions; @@ -393,6 +395,8 @@ abstract class BaseBlock< let resultAfter: AfterResultOut | undefined = undefined; let errorResult: ErrorResultOut | undefined = undefined; + const dep = createDepAccessor(deps); + try { if (step.params) { @@ -401,14 +405,14 @@ abstract class BaseBlock< } // Тут не нужен cancel. - params = step.params({ params: params as unknown as Params, context, deps }); + params = step.params({ params: params as unknown as Params, context, deps, dep }); if (!(params && typeof params === 'object')) { throw createError('Result of options.params must be an object', ERROR_ID.INVALID_OPTIONS_PARAMS); } } if (typeof step.before === 'function') { - resultBefore = await step.before({ cancel, params, context, deps }); + resultBefore = await step.before({ cancel, params, context, deps, dep }); blockCancel.throwIfCancelled(); if (resultBefore instanceof BaseBlock) { @@ -438,7 +442,7 @@ abstract class BaseBlock< blockCancel.throwIfCancelled(); if (typeof step.after === 'function') { - resultAfter = await step.after({ cancel, params, context, deps, result: (resultBefore || resultBlock) as any }); + resultAfter = await step.after({ cancel, params, context, deps, dep, result: (resultBefore || resultBlock) as any }); blockCancel.throwIfCancelled(); if (resultAfter instanceof BaseBlock) { @@ -461,7 +465,7 @@ abstract class BaseBlock< // FIXME: А нужно ли уметь options.error делать асинхронным? // if (typeof step.error === 'function') { - errorResult = step.error({ cancel, params, context, deps, error }); + errorResult = step.error({ cancel, params, context, deps, dep, error }); } else { throw error; } @@ -536,9 +540,11 @@ abstract class BaseBlock< let key; const optionsKey = this.options.key; + const dep = createDepAccessor(deps); + if (cache && optionsKey) { // Тут не нужен cancel. - key = (typeof optionsKey === 'function') ? optionsKey({ params, context, deps }) : optionsKey; + key = (typeof optionsKey === 'function') ? optionsKey({ params, context, deps, dep }) : optionsKey; if (typeof key !== 'string') { key = null; } @@ -595,7 +601,7 @@ export default BaseBlock; // --------------------------------------------------------------------------------------------------------------- // -function extendDeps(deps?: DescriptBlockId | DepsIds | null): DepsIds | null { +function extendDeps(deps?: DescriptBlockId | UntypedId | DepsIds | null): DepsIds | null { if (!deps) { return null; } diff --git a/lib/context.ts b/lib/context.ts index 6738a79..10cba68 100644 --- a/lib/context.ts +++ b/lib/context.ts @@ -5,7 +5,6 @@ import BlockClass from './block'; import type { Deffered } from './getDeferred'; import getDeferred from './getDeferred'; -import type { DescriptBlockId } from './depsDomain'; import type Cancel from './cancel'; import type DepsDomain from './depsDomain'; import type { BlockResultOut, InferResultOrResult } from './types'; @@ -34,8 +33,8 @@ class RunContext< private nBlocks = 0; private nActiveBlocks = 0; - private blockPromises: Record> = {}; - private blockResults: Record> = {}; + private blockPromises: Record> = {}; + private blockResults: Record> = {}; private waitingForDeps: Array = []; @@ -75,7 +74,7 @@ class RunContext< return this.nActiveBlocks--; } - getPromise(id: DescriptBlockId) { + getPromise(id: symbol) { let deferred = this.blockPromises[ id ]; if (!deferred) { const result = this.blockResults[ id ]; @@ -95,7 +94,7 @@ class RunContext< return deferred.promise; } - resolvePromise(id: DescriptBlockId, result: ResultOut) { + resolvePromise(id: symbol, result: ResultOut) { this.blockResults[ id ] = { result: result, }; @@ -107,7 +106,7 @@ class RunContext< } } - rejectPromise(id: DescriptBlockId, error: DescriptError) { + rejectPromise(id: symbol, error: DescriptError) { this.blockResults[ id ] = { error: error, }; diff --git a/lib/depsDomain.ts b/lib/depsDomain.ts index 54b97c6..f3a4432 100644 --- a/lib/depsDomain.ts +++ b/lib/depsDomain.ts @@ -1,25 +1,58 @@ -// TODO как это типизировать any этот? -export type DescriptBlockDeps = Record; -export type DescriptBlockId = symbol; -export type GenerateId = (label?: string) => DescriptBlockId; +declare const __covariant: unique symbol; +declare const __contravariant: unique symbol; +declare const __untyped: unique symbol; + +export type DescriptBlockId = symbol & { + readonly [__covariant]: T; + [__contravariant]: (_: T) => void; +}; + +export type UntypedId = symbol & { readonly [__untyped]: true }; + +type BlockResultOf = B extends { readonly __resultType: infer R } ? R : unknown; + +export type GenerateId = { + (block: B): DescriptBlockId>; + (label?: string): UntypedId; + (label?: string): DescriptBlockId; +}; + +export type DescriptBlockDeps = Record; + +export function dep(deps: DescriptBlockDeps, id: DescriptBlockId): T; +export function dep(deps: DescriptBlockDeps, id: UntypedId): unknown; +export function dep(deps: DescriptBlockDeps, id: symbol): unknown { + return deps[ id ]; +} + +export type DepAccessor = { + (id: DescriptBlockId): T; + (id: UntypedId): unknown; +}; + +export function createDepAccessor(deps: DescriptBlockDeps): DepAccessor { + return ((id: symbol) => deps[ id ]) as DepAccessor; +} class DepsDomain { - ids: Record; + ids: Record; + constructor(parent: any) { this.ids = (parent instanceof DepsDomain) ? Object.create(parent.ids) : {}; - } - generateId: GenerateId = (label?: string): DescriptBlockId => { - const id = Symbol(label); + // Runtime ignores the block argument (used only for type inference). + // Symbol label is taken from a string arg or left undefined. + generateId: GenerateId = ((blockOrLabel?: any): DescriptBlockId => { + const label = typeof blockOrLabel === 'string' ? blockOrLabel : undefined; + const id = Symbol(label) as DescriptBlockId; this.ids[ id ] = true; return id; - }; + }) as GenerateId; - isValidId(id: DescriptBlockId) { + isValidId(id: symbol): boolean { return Boolean(this.ids[ id ]); } - } export default DepsDomain; diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts index a51207d..29234b7 100644 --- a/lib/functionBlock.ts +++ b/lib/functionBlock.ts @@ -1,6 +1,6 @@ import BaseBlock from './block'; -import type { DescriptBlockDeps } from './depsDomain'; -import DepsDomain from './depsDomain'; +import type { DepAccessor, DescriptBlockDeps } from './depsDomain'; +import DepsDomain, { createDepAccessor } from './depsDomain'; import { createError, ERROR_ID } from './error'; import type { BlockResultOut, DescriptBlockOptions, InferParamsOutFromBlock } from './types'; import type ContextClass from './context'; @@ -14,6 +14,7 @@ export type FunctionBlockDefinition< params: Params; context: Context; deps: DescriptBlockDeps; + dep: DepAccessor; generateId: DepsDomain['generateId']; cancel: Cancel; blockCancel: Cancel; @@ -72,6 +73,7 @@ class FunctionBlock< params: params, context: context, deps: deps, + dep: createDepAccessor(deps), generateId: depsDomain.generateId, }), blockCancel.getPromise(), diff --git a/lib/index.ts b/lib/index.ts index b011f78..b87505a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,6 +7,7 @@ import type { LoggerEvent, LoggerInterface } from './logger'; import Cache, { CacheInterface } from './cache'; import request, { type RequestOptions, type GetRetryStrategyParams } from './request'; +import { dep } from './depsDomain'; import type { GenerateId, DescriptBlockDeps, DescriptBlockId } from './depsDomain'; import Block from './block'; import ArrayBlock from './arrayBlock'; @@ -205,6 +206,7 @@ export { DescriptHttpBlockQueryValue, RetryStrategyInterface, GetRetryStrategyParams, + dep, GenerateId, DescriptBlockId, InferResultFromBlock, diff --git a/lib/types.ts b/lib/types.ts index 6fdbc95..57aea6c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,6 @@ import type Cancel from './cancel'; import type BaseBlock from './block'; -import type { DescriptBlockDeps, DescriptBlockId } from './depsDomain'; +import type { DepAccessor, DescriptBlockDeps, DescriptBlockId, UntypedId } from './depsDomain'; import type { DescriptError } from './error'; import type { CacheInterface } from './cache'; import type { IncomingHttpHeaders } from 'http'; @@ -142,7 +142,7 @@ export type Tail = export type Equal = A extends B ? (B extends A ? A : never) : never; -export type DepsIds = Array; +export type DepsIds = Array | UntypedId>; export interface DescriptBlockOptions< Context, ParamsOut, @@ -154,19 +154,21 @@ export interface DescriptBlockOptions< > { name?: string; - id?: DescriptBlockId; - deps?: DescriptBlockId | DepsIds | null; + id?: NoInfer>>> | UntypedId; + deps?: DescriptBlockId | UntypedId | DepsIds | null; params?: (args: { params: Params; context?: Context; deps: DescriptBlockDeps; + dep: DepAccessor; }) => ParamsOut; before?: (args: { params: ParamsOut; context?: Context; deps: DescriptBlockDeps; + dep: DepAccessor; cancel: Cancel; }) => BeforeResultOut; @@ -174,6 +176,7 @@ export interface DescriptBlockOptions< params: ParamsOut; context?: Context; deps: DescriptBlockDeps; + dep: DepAccessor; cancel: Cancel; result: [ unknown ] extends [ Exclude ] ? InferResultOrResult : InferResultOrResult> | InferResultOrResult; @@ -183,6 +186,7 @@ export interface DescriptBlockOptions< params: ParamsOut; context?: Context; deps: DescriptBlockDeps; + dep: DepAccessor; cancel: Cancel; error: DescriptError; }) => ErrorResultOut; @@ -193,6 +197,7 @@ export interface DescriptBlockOptions< params: ParamsOut; context?: Context; deps: DescriptBlockDeps; + dep: DepAccessor; }) => string); maxage?: number; cache?: CacheInterface | ((args: { params: ParamsOut }) => Promise>); diff --git a/tests/httpBlock.test.ts b/tests/httpBlock.test.ts index 8702675..0874464 100644 --- a/tests/httpBlock.test.ts +++ b/tests/httpBlock.test.ts @@ -16,7 +16,7 @@ import type { ServerResponse, ClientRequest } from 'node:http'; import strip_null_and_undefined_values from '../lib/stripNullAndUndefinedValues'; import type { DescriptBlockOptions, DescriptHttpBlockResult } from '../lib/types'; import type { DescriptHttpBlockDescription } from '../lib/httpBlock'; -import type { DescriptBlockId } from '../lib/depsDomain'; +import type { UntypedId } from '../lib/depsDomain'; // --------------------------------------------------------------------------------------------------------------- // describe('http', < @@ -115,7 +115,7 @@ describe('http', < const spy = vi.fn((...args: any) => value); let fooResult; - let id: DescriptBlockId; + let id!: UntypedId; const block = de.func({ block: ({ generateId }) => { id = generateId(); diff --git a/tests/options.deps.test.ts b/tests/options.deps.test.ts index 5ddf9c9..322344b 100644 --- a/tests/options.deps.test.ts +++ b/tests/options.deps.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as de from '../lib'; import { getErrorBlock, getResultBlock, getTimeout, waitForValue } from './helpers'; -import type { DescriptBlockId } from '../lib/depsDomain'; +import type { UntypedId } from '../lib/depsDomain'; // --------------------------------------------------------------------------------------------------------------- // @@ -13,7 +13,7 @@ describe('options.deps', () => { const data = { foo: 42, }; - const id = Symbol('block'); + const id = Symbol('block') as UntypedId; const block = getResultBlock(data, 50).extend({ options: { id: id, @@ -52,7 +52,7 @@ describe('options.deps', () => { it('failed block with id and without deps', async() => { const error = de.error('ERROR'); - const id = Symbol('block'); + const id = Symbol('block') as UntypedId; const block = getErrorBlock(error, 50).extend({ options: { id: id, @@ -74,7 +74,7 @@ describe('options.deps', () => { const data = { foo: 42, }; - const id = Symbol('block'); + const id = Symbol('block') as UntypedId; const block = getResultBlock(data, 50).extend({ options: { deps: id, @@ -100,7 +100,7 @@ describe('options.deps', () => { }; const block = de.func({ block: () => { - const id = Symbol('block'); + const id = Symbol('block') as UntypedId; return getResultBlock(data, 50).extend({ options: { deps: id, @@ -542,7 +542,7 @@ describe('options.deps', () => { const beforeBar = vi.fn(); - let idFoo: DescriptBlockId; + let idFoo!: UntypedId; const block = de.func({ block: ({ generateId }) => { idFoo = generateId(); @@ -570,9 +570,7 @@ describe('options.deps', () => { await de.run(block); const deps = beforeBar.mock.calls[ 0 ][ 0 ].deps; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(deps[ idFoo ]).toBe(dataFoo); + expect(de.dep(deps, idFoo)).toBe(dataFoo); }); it('before( { deps } ) has deps results #2', async() => { @@ -590,8 +588,8 @@ describe('options.deps', () => { const beforeQuu = vi.fn(); - let idFoo: DescriptBlockId; - let idBar: DescriptBlockId; + let idFoo!: UntypedId; + let idBar!: UntypedId; const block = de.func({ block: ({ generateId }) => { idFoo = generateId(); @@ -626,12 +624,8 @@ describe('options.deps', () => { await de.run(block); const deps = beforeQuu.mock.calls[ 0 ][ 0 ].deps; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(deps[ idFoo ]).toBe(dataFoo); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(deps[ idBar ]).toBe(dataBar); + expect(de.dep(deps, idFoo)).toBe(dataFoo); + expect(de.dep(deps, idBar)).toBe(dataBar); }); it('before( { deps } ) has not results from other blocks', async() => { @@ -649,8 +643,8 @@ describe('options.deps', () => { const beforeQuu = vi.fn(); - let idFoo: DescriptBlockId; - let idBar: DescriptBlockId; + let idFoo!: UntypedId; + let idBar!: UntypedId; const block = de.func({ block: ({ generateId }) => { idFoo = generateId(); @@ -685,12 +679,8 @@ describe('options.deps', () => { await de.run(block); const deps = beforeQuu.mock.calls[ 0 ][ 0 ].deps; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(deps[ idFoo ]).toBeUndefined(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(deps[ idBar ]).toBe(dataBar); + expect(de.dep(deps, idFoo)).toBeUndefined(); + expect(de.dep(deps, idBar)).toBe(dataBar); }); it('wait for result of de.func', async() => { @@ -699,7 +689,7 @@ describe('options.deps', () => { const dataFoo = { foo: 42, }; - let idFoo: DescriptBlockId; + let idFoo!: UntypedId; const block = de.func({ block: ({ generateId }) => { @@ -730,9 +720,7 @@ describe('options.deps', () => { await de.run(block); const deps = beforeQuu.mock.calls[ 0 ][ 0 ].deps; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(deps[ idFoo ]).toBe(dataFoo); + expect(de.dep(deps, idFoo)).toBe(dataFoo); }); it('result of de.func has deps #1', async() => { @@ -822,7 +810,7 @@ describe('options.deps', () => { }); it('unresolved deps #1, id is "foo"', async() => { - const id = Symbol('foo'); + const id = Symbol('foo') as UntypedId; const block = getResultBlock(null, 50).extend({ options: { deps: id, @@ -840,7 +828,7 @@ describe('options.deps', () => { }); it('unresolved deps #2, id is "foo"', async() => { - const id = Symbol('foo'); + const id = Symbol('foo') as UntypedId; const blockFoo = getResultBlock(null, 50); const blockBar = getResultBlock(null, 50); diff --git a/tests/options.params.test.ts b/tests/options.params.test.ts index 858a7de..d73e613 100644 --- a/tests/options.params.test.ts +++ b/tests/options.params.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as de from '../lib'; import { getResultBlock } from './helpers'; -import type { DescriptBlockId } from '../lib/depsDomain'; +import type { UntypedId } from '../lib/depsDomain'; describe('options.params', () => { @@ -44,7 +44,7 @@ describe('options.params', () => { const spy = vi.fn(); let dataFoo; - let idFoo: DescriptBlockId; + let idFoo!: UntypedId; const block = de.func({ block: ({ generateId }) => { From 4521bccc79c2d6afc924d686646034a9fa7ff948 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 1 Apr 2026 15:11:24 +0300 Subject: [PATCH 2/2] update docs --- docs/deps.md | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/deps.md b/docs/deps.md index 83f65ce..4015bff 100644 --- a/docs/deps.md +++ b/docs/deps.md @@ -85,9 +85,9 @@ const block = ( { generateId } ) => { Иногда нам просто достаточно, чтобы один блок выполнился после окончания работы другого блока. Но чаще всего, нам нужно результат одного блока использовать для запуска другого блока. -```js +```ts const block = ( { generateId } ) => { - const fooId = generateId(); + const fooId = generateId(blockFoo); return de.object( { block: { @@ -101,13 +101,10 @@ const block = ( { generateId } ) => { options: { deps: fooId, - params: ( { deps } ) => { - // В deps приходят результаты работы всех блоков, - // от которых зависит блок. - // - // Достаем результат blockFoo. - // - const fooResult = deps[ fooId ]; + params: ( { dep } ) => { + // dep — типобезопасный accessor для получения результатов всех блоков, от которых зависит блок. + // Достаем результат blockFoo c сохранением типов. + const fooResult = dep( fooId ); // Используем значение из fooResult в качестве // параметра запроса блока blockBar. @@ -124,8 +121,10 @@ const block = ( { generateId } ) => { }; ``` -Если у блока были зависимости, то во все "хуки" (`options.params`, `options.before`, ...) в поле `deps` -будет приходить объект с результатами этих зависимостей. +Если у блока были зависимости, то во все "хуки" (`options.params`, `options.before`, ...) в аргументах приходят: + +- `deps` — объект `Record` с результатами всех зависимостей +- `dep` — типобезопасный accessor: `dep(id)` эквивалентен `deps[id]`, но сохраняет тип конкретной зависимости ## `generateId()` @@ -154,6 +153,24 @@ id-шники, сгенерированные при помощи `generateId`, И при попытке использовать какое-либо другое значение в качестве id, блок завершится с ошибкой `de.ERROR_ID.INVALID_DEPS_ID`. +### Типизированный `generateId` + +`generateId` поддерживает несколько перегрузок: + +```ts +// Нетипизированный id (UntypedId) — dep(id) вернет unknown +const id = generateId(); +const id = generateId('label'); + +// Типизированный id с явным типом — dep(id) вернет MyType +const id = generateId(); + +// Типизированный id, тип выведен из блока — dep(id) вернет тип результата blockFoo +const id = generateId(blockFoo); +``` + + + ## Ошибки `de.ERROR_ID.DEPS_ERROR` и `de.ERROR_ID.DEPS_NOT_RESOLVED` Обе ошибки связаны с проблема при разрешении зависимостей, но случаются в немного разных ситуациях: