diff --git a/deno.json b/deno.json index 1c913fe6b..cba3abf5e 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,7 @@ "name": "@effection/effection", "exports": "./mod.ts", "license": "ISC", - "publish": { "include": ["lib", "mod.ts", "README.md"] }, + "publish": { "include": ["lib", "mod.ts", "experimental.ts", "README.md"] }, "lock": false, "tasks": { "test": "deno test --allow-run --allow-read --allow-env --allow-ffi", @@ -29,6 +29,10 @@ "tinyexec": "npm:tinyexec@1.0.1", "https://deno.land/x/path_to_regexp@v6.2.1/index.ts": "npm:path-to-regexp@8.2.0" }, + "exports": { + ".": "./mod.ts", + "./experimental": "./experimental.ts" + }, "nodeModulesDir": "auto", "workspace": [ "./www" diff --git a/experimental.ts b/experimental.ts new file mode 100644 index 000000000..facf7fd39 --- /dev/null +++ b/experimental.ts @@ -0,0 +1,7 @@ +import Scope from "./lib/api/scope.ts"; +import Task from "./lib/api/task.ts"; +import Main from "./lib/api/main.ts"; + +export * from "./lib/api.ts"; + +export const api = { Scope, Task, Main }; diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 000000000..004d89de6 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,121 @@ +// deno-lint-ignore-file ban-types +import { createContext } from "./context.ts"; +import { useScope } from "./scope.ts"; +import type { Api, Decorate, Operation, Scope } from "./types.ts"; +import type { ScopeInternal } from "./scope-internal.ts"; + +export interface Middleware { + (args: TArgs, next: (...args: TArgs) => TReturn): TReturn; +} + +export function createApi(name: string, core: A): Api { + let fields = Object.keys(core) as (keyof A)[]; + + let context = createContext(`api::${name}`) as Api["context"]; + + let api: Api = { + core, + context, + lookup: (scope) => createHandle(api, scope), + decorate: (decorator, options = { at: "max" }) => ({ + *[Symbol.iterator]() { + let scope = yield* useScope(); + scope.decorate(api, decorator, options); + }, + }), + operations: fields.reduce((sum, field) => { + if (typeof core[field] === "function") { + return Object.assign(sum, { + // deno-lint-ignore no-explicit-any + [field]: (...args: any[]) => ({ + *[Symbol.iterator]() { + let scope = yield* useScope(); + let handle = api.lookup(scope); + let handler = handle[field] as Function; + let target = handler(...args); + return isOperation(target) ? yield* target : target; + }, + }), + }); + } else { + return Object.assign(sum, { + [field]: { + *[Symbol.iterator]() { + let scope = yield* useScope(); + let handle = api.lookup(scope); + let target = handle[field]; + return isOperation(target) ? yield* target : target; + }, + }, + }); + } + }, {} as Api["operations"]), + }; + return api; +} + +function createHandle(api: Api, scope: Scope): A { + let handle = Object.create(api.core); + let $scope = scope as ScopeInternal; + for (let key of Object.keys(api.core) as Array) { + let dispatch = (args: unknown[], next: (...args: unknown[]) => unknown) => { + let { min, max } = $scope.reduce(api.context, (sum, current) => { + let min = current.min.flatMap((around) => + around[key] ? [around[key]] : [] + ); + let max = current.max.flatMap((around) => + around[key] ? [around[key]] : [] + ); + + sum.min.push(...min); + sum.max.unshift(...max); + + return sum; + }, { + min: [] as Decorate[typeof key][], + max: [] as Decorate[typeof key][], + }); + + let stack = combine(max.concat(min) as Middleware[]); + return stack(args, next); + }; + + if (typeof api.core[key] === "function") { + handle[key] = (...args: unknown[]) => + dispatch(args, api.core[key] as (...args: unknown[]) => unknown); + } else { + Object.defineProperty(handle, key, { + enumerable: true, + get() { + return dispatch([], () => api.core[key]); + }, + }); + } + } + return handle; +} + +function isOperation( + target: Operation | T, +): target is Operation { + return target && !isNativeIterable(target) && + typeof (target as Operation)[Symbol.iterator] === "function"; +} + +function isNativeIterable(target: unknown): boolean { + return ( + typeof target === "string" || Array.isArray(target) || + target instanceof Map || target instanceof Set + ); +} + +function combine( + middlewares: Middleware[], +): Middleware { + if (middlewares.length === 0) { + return (args, next) => next(...args); + } + return middlewares.reduceRight((sum, middleware) => (args, next) => + middleware(args, (...args) => sum(args, next)) + ); +} diff --git a/lib/api/main.ts b/lib/api/main.ts new file mode 100644 index 000000000..ff6a81f19 --- /dev/null +++ b/lib/api/main.ts @@ -0,0 +1,12 @@ +import { createApi } from "../api.ts"; +import type { Api, Operation } from "../types.ts"; + +export interface Main { + main: (body: (args: string[]) => Operation) => Promise; +} + +export default createApi
("Main", { + main() { + throw new TypeError(`missing handler for "main()"`); + }, +}) as Api
; diff --git a/lib/api/scope.ts b/lib/api/scope.ts new file mode 100644 index 000000000..7ec80c059 --- /dev/null +++ b/lib/api/scope.ts @@ -0,0 +1,22 @@ +import { createApi } from "../api.ts"; +import type { Api, Context, Operation, Scope } from "../types.ts"; + +export interface ScopeApi { + create(parent: Scope): [Scope, () => Operation]; + destroy(scope: Scope): Operation; + set(contexts: Record, context: Context, value: T): T; + delete(contexts: Record, context: Context): boolean; +} + +export default createApi("Scope", { + create() { + throw new TypeError(`no implementation for Scope.create()`); + }, + *destroy() {}, + set(contexts, context, value) { + return contexts[context.name] = value; + }, + delete(contexts, context): boolean { + return delete contexts[context.name]; + }, +}) as Api; diff --git a/lib/api/task.ts b/lib/api/task.ts new file mode 100644 index 000000000..9db28ba8b --- /dev/null +++ b/lib/api/task.ts @@ -0,0 +1,10 @@ +import { createApi } from "../api.ts"; +import type { Api, Operation } from "../types.ts"; + +export interface TaskApi { + run: (operation: () => Operation) => Operation; +} + +export default createApi("Task", { + run: (operation) => operation(), +}) as Api; diff --git a/lib/coroutine.ts b/lib/coroutine.ts index f0c6a1bfa..5d72b2fbb 100644 --- a/lib/coroutine.ts +++ b/lib/coroutine.ts @@ -31,11 +31,12 @@ export function createCoroutine( next(result) { routine.data.exit((exitResult) => { routine.data.exit = (didExit) => didExit(Ok()); + let validator = scope.get(DelimiterContext)?.validator ?? valid; reducer.reduce([ scope.expect(Priority), routine, exitResult.ok ? result : exitResult, - scope.expect(DelimiterContext).validator, + validator, "next", ]); }); @@ -43,11 +44,12 @@ export function createCoroutine( return(result) { routine.data.exit((exitResult) => { routine.data.exit = (didExit) => didExit(Ok()); + let validator = scope.get(DelimiterContext)?.validator ?? valid; reducer.reduce([ scope.expect(Priority), routine, exitResult.ok ? result : exitResult, - scope.expect(DelimiterContext).validator, + validator, "return", ]); }); @@ -66,3 +68,5 @@ export function* useCoroutine(): Operation { }, }) as Coroutine; } + +const valid = () => true; diff --git a/lib/main.ts b/lib/main.ts index 065d39caa..e1f2e3920 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -2,8 +2,9 @@ import { createContext } from "./context.ts"; import type { Operation } from "./types.ts"; import { callcc } from "./callcc.ts"; import { run } from "./run.ts"; -import { useScope } from "./scope.ts"; +import { global, useScope } from "./scope.ts"; import { call } from "./call.ts"; +import api from "./api/main.ts"; /** * Halt process execution immediately and initiate shutdown. If a message is @@ -58,103 +59,205 @@ export function* exit(status: number, message?: string): Operation { * @returns a promise that resolves right after the program exits */ -export async function main( - body: (args: string[]) => Operation, -): Promise { - let hardexit = (_status: number) => {}; - - let result = await run(() => - callcc(function* (resolve) { - // action will return shutdown immediately upon resolve, so stash - // this function in the exit context so it can be called anywhere. - yield* ExitContext.set(resolve); - - // this will hold the event loop and prevent runtimes such as - // Node and Deno from exiting prematurely. - let interval = setInterval(() => {}, Math.pow(2, 30)); - - let scope = yield* useScope(); - - try { - let interrupt = { - SIGINT: () => - scope.run(() => resolve({ status: 130, signal: "SIGINT" })), - SIGTERM: () => - scope.run(() => resolve({ status: 143, signal: "SIGTERM" })), - }; - - yield* withHost({ - *deno() { - hardexit = (status) => Deno.exit(status); - try { - Deno.addSignalListener("SIGINT", interrupt.SIGINT); - /** - * Windows only supports ctrl-c (SIGINT), ctrl-break (SIGBREAK), and ctrl-close (SIGUP) - */ - if (Deno.build.os !== "windows") { - Deno.addSignalListener("SIGTERM", interrupt.SIGTERM); - } - yield* body(Deno.args.slice()); - } finally { - Deno.removeSignalListener("SIGINT", interrupt.SIGINT); - if (Deno.build.os !== "windows") { - Deno.removeSignalListener("SIGTERM", interrupt.SIGTERM); +export function main(body: (args: string[]) => Operation): Promise { + return api.lookup(global).main(body); +} + +// this is the core core main function +global.decorate(api, { + async main([body]) { + let hardexit = (_status: number) => {}; + + let result = await run(() => + callcc(function* (resolve) { + // action will return shutdown immediately upon resolve, so stash + // this function in the exit context so it can be called anywhere. + yield* ExitContext.set(resolve); + + // this will hold the event loop and prevent runtimes such as + // Node and Deno from exiting prematurely. + let interval = setInterval(() => {}, Math.pow(2, 30)); + + let scope = yield* useScope(); + + try { + let interrupt = { + SIGINT: () => + scope.run(() => resolve({ status: 130, signal: "SIGINT" })), + SIGTERM: () => + scope.run(() => resolve({ status: 143, signal: "SIGTERM" })), + }; + + yield* withHost({ + *deno() { + hardexit = (status) => Deno.exit(status); + try { + Deno.addSignalListener("SIGINT", interrupt.SIGINT); + /** + * Windows only supports ctrl-c (SIGINT), ctrl-break (SIGBREAK), and ctrl-close (SIGUP) + */ + if (Deno.build.os !== "windows") { + Deno.addSignalListener("SIGTERM", interrupt.SIGTERM); + } + yield* body(Deno.args.slice()); + } finally { + Deno.removeSignalListener("SIGINT", interrupt.SIGINT); + if (Deno.build.os !== "windows") { + Deno.removeSignalListener("SIGTERM", interrupt.SIGTERM); + } } - } - }, - *node() { - // Annotate dynamic import so that webpack ignores it. - // See https://webpack.js.org/api/module-methods/#webpackignore - let { default: process } = yield* call(() => - import(/* webpackIgnore: true */ "node:process") - ); - hardexit = (status) => process.exit(status); - try { - process.on("SIGINT", interrupt.SIGINT); - if (process.platform !== "win32") { - process.on("SIGTERM", interrupt.SIGTERM); + }, + *node() { + // Annotate dynamic import so that webpack ignores it. + // See https://webpack.js.org/api/module-methods/#webpackignore + let { default: process } = yield* call(() => + import(/* webpackIgnore: true */ "node:process") + ); + hardexit = (status) => process.exit(status); + try { + process.on("SIGINT", interrupt.SIGINT); + if (process.platform !== "win32") { + process.on("SIGTERM", interrupt.SIGTERM); + } + yield* body(process.argv.slice(2)); + } finally { + process.off("SIGINT", interrupt.SIGINT); + if (process.platform !== "win32") { + process.off("SIGTERM", interrupt.SIGINT); + } } - yield* body(process.argv.slice(2)); - } finally { - process.off("SIGINT", interrupt.SIGINT); - if (process.platform !== "win32") { - process.off("SIGTERM", interrupt.SIGINT); + }, + *browser() { + try { + self.addEventListener("unload", interrupt.SIGINT); + yield* body([]); + } finally { + self.removeEventListener("unload", interrupt.SIGINT); } - } - }, - *browser() { - try { - self.addEventListener("unload", interrupt.SIGINT); - yield* body([]); - } finally { - self.removeEventListener("unload", interrupt.SIGINT); - } - }, - }); - - yield* exit(0); - } catch (error) { - yield* resolve({ status: 1, error: error as Error }); - } finally { - clearInterval(interval); + }, + }); + + yield* exit(0); + } catch (error) { + yield* resolve({ status: 1, error: error as Error }); + } finally { + clearInterval(interval); + } + }) + ); + + if (result.message) { + if (result.status === 0) { + console.log(result.message); + } else { + console.error(result.message); } - }) - ); - - if (result.message) { - if (result.status === 0) { - console.log(result.message); - } else { - console.error(result.message); } - } - if (result.error) { - console.error(result.error); - } + if (result.error) { + console.error(result.error); + } - hardexit(result.status); -} + hardexit(result.status); + }, +}, { at: "min" }); +// export async function main( +// body: (args: string[]) => Operation, +// ): Promise { +// let hardexit = (_status: number) => {}; + +// let result = await run(function* () { +// return yield* callcc(function* (resolve) { +// // action will return shutdown immediately upon resolve, so stash +// // this function in the exit context so it can be called anywhere. +// yield* ExitContext.set(resolve); + +// // this will hold the event loop and prevent runtimes such as +// // Node and Deno from exiting prematurely. +// let interval = setInterval(() => {}, Math.pow(2, 30)); + +// let scope = yield* useScope(); + +// try { +// let interrupt = { +// SIGINT: () => +// scope.run(() => resolve({ status: 130, signal: "SIGINT" })), +// SIGTERM: () => +// scope.run(() => resolve({ status: 143, signal: "SIGTERM" })), +// }; + +// yield* withHost({ +// *deno() { +// hardexit = (status) => Deno.exit(status); +// try { +// Deno.addSignalListener("SIGINT", interrupt.SIGINT); +// /** +// * Windows only supports ctrl-c (SIGINT), ctrl-break (SIGBREAK), and ctrl-close (SIGUP) +// */ +// if (Deno.build.os !== "windows") { +// Deno.addSignalListener("SIGTERM", interrupt.SIGTERM); +// } +// yield* body(Deno.args.slice()); +// } finally { +// Deno.removeSignalListener("SIGINT", interrupt.SIGINT); +// if (Deno.build.os !== "windows") { +// Deno.removeSignalListener("SIGTERM", interrupt.SIGTERM); +// } +// } +// }, +// *node() { +// // Annotate dynamic import so that webpack ignores it. +// // See https://webpack.js.org/api/module-methods/#webpackignore +// let { default: process } = yield* call(() => +// import(/* webpackIgnore: true */ "node:process") +// ); +// hardexit = (status) => process.exit(status); +// try { +// process.on("SIGINT", interrupt.SIGINT); +// if (process.platform !== "win32") { +// process.on("SIGTERM", interrupt.SIGTERM); +// } +// yield* body(process.argv.slice(2)); +// } finally { +// process.off("SIGINT", interrupt.SIGINT); +// if (process.platform !== "win32") { +// process.off("SIGTERM", interrupt.SIGINT); +// } +// } +// }, +// *browser() { +// try { +// self.addEventListener("unload", interrupt.SIGINT); +// yield* body([]); +// } finally { +// self.removeEventListener("unload", interrupt.SIGINT); +// } +// }, +// }); + +// yield* exit(0); +// } catch (error) { +// yield* resolve({ status: 1, error: error as Error }); +// } finally { +// clearInterval(interval); +// } +// }); +// }); + +// if (result.message) { +// if (result.status === 0) { +// console.log(result.message); +// } else { +// console.error(result.message); +// } +// } + +// if (result.error) { +// console.error(result.error); +// } + +// hardexit(result.status); +// } const ExitContext = createContext<(exit: Exit) => Operation>("exit"); diff --git a/lib/scope-internal.ts b/lib/scope-internal.ts index 030cc6f36..0e72a6c44 100644 --- a/lib/scope-internal.ts +++ b/lib/scope-internal.ts @@ -1,25 +1,58 @@ +// deno-lint-ignore-file ban-types import { Children, Priority } from "./contexts.ts"; import { Err, Ok, unbox } from "./result.ts"; import { createTask } from "./task.ts"; -import type { Context, Operation, Scope, Task } from "./types.ts"; +import type { + Api, + Context, + Decorate, + DecorateOptions, + Operation, + Scope, + Task, +} from "./types.ts"; import { type WithResolvers, withResolvers } from "./with-resolvers.ts"; -export function createScopeInternal( +import api from "./api/scope.ts"; +import { createCoroutine } from "./coroutine.ts"; + +export function createScopeInternal(parent?: Scope): [ScopeInternal, () => Operation] { + if (!parent) { + let [global, destroy] = buildScopeInternal(); + global.decorate(api, { + create([parent]) { + return buildScopeInternal(parent); + }, + }, { at: "min" }); + return [global, destroy] as const; + } else { + return api.lookup(parent).create(parent) as [ + ScopeInternal, + () => Operation, + ]; + } +} + +function buildScopeInternal( parent?: Scope, ): [ScopeInternal, () => Operation] { let destructors = new Set<() => Operation>(); - let contexts: Record = Object.create( + let contexts = Object.create( parent ? (parent as ScopeInternal).contexts : null, ); + let scope: ScopeInternal = Object.create({ [Symbol.toStringTag]: "Scope", contexts, get(context: Context): T | undefined { return (contexts[context.name] ?? context.defaultValue) as T | undefined; }, + super(context: Context): T | undefined { + return parent?.get(context); + }, set(context: Context, value: T): T { - return contexts[context.name] = value; + return api.lookup(scope).set(contexts, context, value); }, expect(context: Context): T { let value = scope.get(context); @@ -31,7 +64,7 @@ export function createScopeInternal( return value; }, delete(context: Context): boolean { - return delete contexts[context.name]; + return api.lookup(scope).delete(contexts, context); }, hasOwn(context: Context): boolean { return !!Reflect.getOwnPropertyDescriptor(contexts, context.name); @@ -51,52 +84,95 @@ export function createScopeInternal( }; }, + decorate( + api: Api, + decorator: Partial>, + options?: DecorateOptions, + ) { + if (!scope.hasOwn(api.context)) { + scope.set(api.context, { min: [], max: [] }); + } + + let { min, max } = scope.expect(api.context); + + if (options?.at === "min") { + min.push(decorator); + } else { + max.push(decorator); + } + }, + ensure(op: () => Operation): () => void { destructors.add(op); return () => destructors.delete(op); }, + + reduce( + context: Context, + fn: (sum: S, item: T) => S, + initial: S, + ): S { + let sum = initial; + let current = contexts; + while (current) { + if (Object.hasOwn(current, context.name)) { + let item = current[context.name] as T; + if (item) { + sum = fn(sum, item); + } + } + + current = Object.getPrototypeOf(current); + } + return sum; + }, }); scope.set(Priority, scope.expect(Priority) + 1); scope.set(Children, new Set()); parent?.expect(Children).add(scope); + let destroy = () => api.lookup(scope).destroy(scope); + let unbind = parent ? (parent as ScopeInternal).ensure(destroy) : () => {}; let destruction: WithResolvers | undefined = undefined; - function* destroy(): Operation { - if (destruction) { - return yield* destruction.operation; - } - destruction = withResolvers(); - parent?.expect(Children).delete(scope); - unbind(); - let outcome = Ok(); - try { - for (let destructor of destructors) { - try { - destructors.delete(destructor); - yield* destructor(); - } catch (error) { - outcome = Err(error as Error); - } + scope.decorate(api, { + *destroy() { + if (destruction) { + return yield* destruction.operation; } - } finally { - if (outcome.ok) { - destruction.resolve(); - } else { - destruction.reject(outcome.error); + destruction = withResolvers(); + parent?.expect(Children).delete(scope); + unbind(); + let outcome = Ok(); + try { + for (let destructor of destructors) { + try { + destructors.delete(destructor); + yield* destructor(); + } catch (error) { + outcome = Err(error as Error); + } + } + } finally { + if (outcome.ok) { + destruction.resolve(); + } else { + destruction.reject(outcome.error); + } } - } - unbox(outcome); - } + unbox(outcome); + }, + }, { at: "min" }); return [scope, destroy]; } export interface ScopeInternal extends Scope { contexts: Record; + reduce(context: Context, fn: (sum: S, item: T) => S, initial: S): S; ensure(op: () => Operation): () => void; } diff --git a/lib/task.ts b/lib/task.ts index b15e5130a..be8064815 100644 --- a/lib/task.ts +++ b/lib/task.ts @@ -8,6 +8,7 @@ import { createScopeInternal, type ScopeInternal } from "./scope-internal.ts"; import type { Coroutine, Operation, Scope, Task } from "./types.ts"; import { encapsulate, TaskGroupContext } from "./task-group.ts"; import { useScope } from "./scope.ts"; +import api from "./api/task.ts"; export interface TaskOptions { owner: ScopeInternal; @@ -66,7 +67,9 @@ export function createTask(options: TaskOptions): NewTask { }, }) as Task; - let top = new Delimiter(() => encapsulate(operation)); + let top = new Delimiter(() => + api.lookup(scope).run(() => encapsulate(operation)) + ); scope.set(DelimiterContext, top as Delimiter); let group = scope.expect(TaskGroupContext); @@ -117,7 +120,7 @@ export function* trap(operation: () => Operation): Operation { let original = { error: scope.expect(ErrorContext), - delimiter: scope.expect(DelimiterContext), + delimiter: scope.get(DelimiterContext), }; let delimiter = new Delimiter(operation, original.delimiter); @@ -136,7 +139,7 @@ export function* trap(operation: () => Operation): Operation { if (outcome.exists) { resolve(outcome.value); } else { - original.delimiter.interrupt(); + original.delimiter?.interrupt(); } return (didExit) => didExit(Ok()); }, diff --git a/lib/types.ts b/lib/types.ts index 34752bb6c..b73dcaa66 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -237,6 +237,40 @@ export interface Context { with(value: T, operation: (value: T) => Operation): Operation; } +export interface Middleware { + (args: TArgs, next: (...args: TArgs) => TReturn): TReturn; +} + +export interface Api { + context: Context<{ + min: Partial>[]; + max: Partial>[]; + }>; + core: A; + lookup: (scope: Scope) => A; + operations: { + [K in keyof A]: A[K] extends Operation ? A[K] + : A[K] extends (...args: infer TArgs) => infer TReturn + ? TReturn extends Operation ? A[K] + : (...args: TArgs) => Operation + : Operation; + }; + decorate: ( + decorate: Partial>, + options?: DecorateOptions, + ) => Operation; +} + +export interface DecorateOptions { + at: "min" | "max"; +} + +export type Decorate = { + [K in keyof Api]: Api[K] extends (...args: infer TArgs) => infer TReturn + ? Middleware + : Middleware<[], Api[K]>; +}; + /** * A programatic API to interact with an Effection scope from outside of an * {@link Operation}. @@ -288,6 +322,11 @@ export interface Scope { */ spawn(operation: () => Operation): Operation>; + /** + * TODO + */ + eval(operation: () => Operation): Operation; + /** * Get a {@link Context} value from outside of an operation. * @@ -296,6 +335,8 @@ export interface Scope { */ get(context: Context): T | undefined; + super(context: Context): T | undefined; + /** * Set the value of a {@link Context} from outside of an operation * @@ -327,6 +368,12 @@ export interface Scope { * @returns `true` if scope has its own context, `false` if context is not present, or inherited from its parent. */ hasOwn(context: Context): boolean; + + decorate( + api: Api, + decorator: Partial>, + opts?: DecorateOptions, + ): void; } /** diff --git a/tasks/build-npm.ts b/tasks/build-npm.ts index 51ea9acff..96d391eaa 100644 --- a/tasks/build-npm.ts +++ b/tasks/build-npm.ts @@ -1,5 +1,5 @@ -import { build, emptyDir } from "jsr:@deno/dnt@0.41.3"; - +import { build, emptyDir } from "jsr:@deno/dnt@0.42.3"; +import denoJSON from "../deno.json" with { type: "json" }; const outDir = "./build/npm"; await emptyDir(outDir); @@ -10,7 +10,10 @@ if (!version) { } await build({ - entryPoints: ["./mod.ts"], + entryPoints: Object.entries(denoJSON.exports).map(([key, value]) => ({ + name: key, + path: value, + })), outDir, shims: { deno: false, diff --git a/test/api.test.ts b/test/api.test.ts new file mode 100644 index 000000000..c18fe4c78 --- /dev/null +++ b/test/api.test.ts @@ -0,0 +1,210 @@ +import { createApi } from "../lib/api.ts"; +import { constant } from "../lib/constant.ts"; +import { type Operation, spawn } from "../lib/mod.ts"; +import { run } from "../lib/run.ts"; +import { describe, expect, it } from "./suite.ts"; + +describe("api", () => { + it("invokes operation functions as operations", async () => { + let api = createApi("test", { + *test() { + return 5; + }, + }); + + await run(function* () { + expect(yield* api.operations.test()).toEqual(5); + }); + }); + + it("invokes synchronous functions as operations", async () => { + let api = createApi("test", { + five: () => 5, + }); + + await run(function* () { + expect(yield* api.operations.five()).toEqual(5); + }); + }); + + it("invokes operations as operations", async () => { + let api = createApi("test", { + five: { + *[Symbol.iterator]() { + return 5; + }, + } as Operation, + }); + + await run(function* () { + expect(yield* api.operations.five).toEqual(5); + }); + }); + + it("invokes constants as operations", async () => { + let api = createApi("test", { + five: 5, + }); + + await run(function* () { + expect(yield* api.operations.five).toEqual(5); + }); + }); + + it("can have middleware installed", async () => { + let api = createApi("test", { + constFive: 5, + *operationFnFive() { + return 5; + }, + operationFive: constant(5), + syncFive: () => 5 as number, + }); + + await run(function* () { + yield* api.decorate({ + constFive(args, next) { + return next(...args) * 2; + }, + *operationFnFive(args, next) { + return (yield* next(...args)) * 2; + }, + *operationFive(args, next) { + return (yield* next(...args)) * 2; + }, + syncFive: (args, next) => next(...args) * 2, + }); + + expect(yield* api.operations.constFive).toEqual(10); + expect(yield* api.operations.operationFnFive()).toEqual(10); + expect(yield* api.operations.operationFive).toEqual(10); + expect(yield* api.operations.syncFive()).toEqual(10); + }); + }); + + it("inherits middleware from scope", async () => { + let api = createApi("test", { + *num(value: number): Operation { + return value; + }, + }); + + await run(function* () { + yield* api.decorate({ + *num(args, next) { + return (yield* next(...args)) * 2; + }, + }); + let task = yield* spawn(function* () { + return yield* api.operations.num(5); + }); + + expect(yield* task).toEqual(10); + }); + }); + + it("applies maximal middleware before minimal middleware", async () => { + let api = createApi("test", { + *test(order: string[]): Operation { + return order; + }, + }); + + await run(function* () { + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("max1")); + return output.concat("/max1"); + }, + }); + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("max2")); + return output.concat("/max2"); + }, + }); + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("min1")); + return output.concat("/min1"); + }, + }, { at: "min" }); + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("min2")); + return output.concat("/min2"); + }, + }, { at: "min" }); + + expect(yield* api.operations.test([])).toEqual([ + "max1", + "max2", + "min1", + "min2", + "/min2", + "/min1", + "/max2", + "/max1", + ]); + }); + }); + + it("applies outer scope maxima more maximally than inner scopes maxima", async () => { + let api = createApi("test", { + *test(order: string[]): Operation { + return order; + }, + }); + + await run(function* outer() { + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("outermax")); + return output.concat("/outermax"); + }, + }); + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("outermin")); + return output.concat("/outermin"); + }, + }, { at: "min" }); + + let task = yield* spawn(function* inner() { + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("innermax")); + return output.concat("/innermax"); + }, + }); + yield* api.decorate({ + *test(args, next) { + let [input] = args; + let output = yield* next(input.concat("innermin")); + return output.concat("/innermin"); + }, + }, { at: "min" }); + + return yield* api.operations.test([]); + }); + + expect(yield* task).toEqual([ + "outermax", + "innermax", + "innermin", + "outermin", + "/outermin", + "/innermin", + "/innermax", + "/outermax", + ]); + }); + }); +}); diff --git a/test/scope.test.ts b/test/scope.test.ts index 515c68530..f2dfc24d3 100644 --- a/test/scope.test.ts +++ b/test/scope.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "./suite.ts"; import { createContext, createScope, + global, resource, run, sleep, diff --git a/test/suite.ts b/test/suite.ts index c07fbf33f..4d4c6cde8 100644 --- a/test/suite.ts +++ b/test/suite.ts @@ -1,6 +1,6 @@ import { expect } from "@std/expect"; import { ctrlc } from "ctrlc-windows"; -import { type KillSignal, type Options, type Output, x as $x } from "tinyexec"; +import type { KillSignal, Options, Output } from "tinyexec"; export { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; export { expectType } from "ts-expect"; export { expect }; @@ -94,6 +94,7 @@ export function x( options?: Partial, ): Operation { return resource(function* (provide) { + let { x: $x } = yield* until(import("tinyexec")); let tinyexec = $x(cmd, args, { ...options }); let promise: Promise = tinyexec as unknown as Promise;