diff --git a/lib/scope-internal.ts b/lib/scope-internal.ts new file mode 100644 index 00000000..1fe2be49 --- /dev/null +++ b/lib/scope-internal.ts @@ -0,0 +1,86 @@ +import { Children, Generation } from "./contexts.ts"; +import { Err, Ok, unbox } from "./result.ts"; +import { createTask } from "./task.ts"; +import type { Context, Operation, Scope, Task } from "./types.ts"; + +export function createScopeInternal( + parent?: Scope, +): [ScopeInternal, () => Operation] { + let destructors = new Set<() => Operation>(); + + let contexts: Record = 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; + }, + set(context: Context, value: T): T { + return contexts[context.name] = value; + }, + expect(context: Context): T { + let value = scope.get(context); + if (typeof value === "undefined") { + let error = new Error(context.name); + error.name = `MissingContextError`; + throw error; + } + return value; + }, + delete(context: Context): boolean { + return delete contexts[context.name]; + }, + hasOwn(context: Context): boolean { + return !!Reflect.getOwnPropertyDescriptor(contexts, context.name); + }, + run(operation: () => Operation): Task { + let { task, start } = createTask({ operation, owner: scope }); + start(); + return task; + }, + spawn(operation: () => Operation): Operation> { + return { + *[Symbol.iterator]() { + let { task, start } = createTask({ operation, owner: scope }); + start(); + return task; + }, + }; + }, + + ensure(op: () => Operation): () => void { + destructors.add(op); + return () => destructors.delete(op); + }, + }); + + scope.set(Generation, scope.expect(Generation) + 1); + scope.set(Children, new Set()); + parent?.expect(Children).add(scope); + + let unbind = parent ? (parent as ScopeInternal).ensure(destroy) : () => {}; + + function* destroy(): Operation { + parent?.expect(Children).delete(scope); + unbind(); + let outcome = Ok(); + for (let destructor of [...destructors].reverse()) { + try { + destructors.delete(destructor); + yield* destructor(); + } catch (error) { + outcome = Err(error as Error); + } + } + unbox(outcome); + } + + return [scope, destroy]; +} + +export interface ScopeInternal extends Scope { + contexts: Record; + ensure(op: () => Operation): () => void; +} diff --git a/lib/scope.ts b/lib/scope.ts index a05fc302..780c4368 100644 --- a/lib/scope.ts +++ b/lib/scope.ts @@ -1,19 +1,44 @@ -import { Children, Generation } from "./contexts.ts"; -import type { - Context, - Effect, - Future, - Operation, - Scope, - Task, -} from "./types.ts"; -import { Err, Ok, unbox } from "./result.ts"; -import { createTask } from "./task.ts"; - -const scope: [ScopeInternal, () => Operation] = createScopeInternal(); - -export const global = scope[0]; - +import type { Effect, Future, Operation, Scope } from "./types.ts"; +import { Ok } from "./result.ts"; +import { createScopeInternal } from "./scope-internal.ts"; + +/** + * The root of all Effection Scopes. + */ +export const [global] = createScopeInternal(); + +/** + * Create a new {@link Scope} as a child of `parent`, inheriting all its contexts. + * along with a method to destroy the scope. Whenever the scope is destroyd, all + * tasks and resources it contains will be halted. + * + * This function is used mostly by frameworks as an intergration point to enter + * Effection. + * + * @example + * ```js + * import { createScope, sleep, suspend } from "effection"; + * + * let [scope, destroy] = createScope(); + * + * let delay = scope.run(function*() { + * yield* sleep(1000); + * }); + * scope.run(function*() { + * try { + * yield* suspend(); + * } finally { + * console.log('done!') + * } + * }); + * await delay; + * await destroy(); // prints "done!"; + * ``` + * + * @param parent scope. If no parent is specified it will derive directly from {@link global} + * @returns a tuple containing the freshly created scope, along with a function to + * destroy it. + */ export function createScope( parent: Scope = global, ): [Scope, () => Future] { @@ -21,88 +46,11 @@ export function createScope( return [scope, () => parent.run(destroy)]; } -export function createScopeInternal( - parent?: Scope, -): [ScopeInternal, () => Operation] { - let destructors = new Set<() => Operation>(); - - let contexts: Record = 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; - }, - set(context: Context, value: T): T { - return contexts[context.name] = value; - }, - expect(context: Context): T { - let value = scope.get(context); - if (typeof value === "undefined") { - let error = new Error(context.name); - error.name = `MissingContextError`; - throw error; - } - return value; - }, - delete(context: Context): boolean { - return delete contexts[context.name]; - }, - hasOwn(context: Context): boolean { - return !!Reflect.getOwnPropertyDescriptor(contexts, context.name); - }, - run(operation: () => Operation): Task { - let { task, start } = createTask({ operation, owner: scope }); - start(); - return task; - }, - spawn(operation: () => Operation): Operation> { - return { - *[Symbol.iterator]() { - let { task, start } = createTask({ operation, owner: scope }); - start(); - return task; - }, - }; - }, - - ensure(op: () => Operation): () => void { - destructors.add(op); - return () => destructors.delete(op); - }, - }); - - scope.set(Generation, scope.expect(Generation) + 1); - scope.set(Children, new Set()); - parent?.expect(Children).add(scope); - - let unbind = parent ? (parent as ScopeInternal).ensure(destroy) : () => {}; - - function* destroy(): Operation { - parent?.expect(Children).delete(scope); - unbind(); - let outcome = Ok(); - for (let destructor of [...destructors].reverse()) { - try { - destructors.delete(destructor); - yield* destructor(); - } catch (error) { - outcome = Err(error as Error); - } - } - unbox(outcome); - } - - return [scope, destroy]; -} - -export interface ScopeInternal extends Scope { - contexts: Record; - ensure(op: () => Operation): () => void; -} - +/** + * Get the scope of the currently running {@link Operation}. + * + * @returns an operation yielding the current scope + */ export function* useScope(): Operation { return (yield { description: `useScope()`, diff --git a/lib/scoped.ts b/lib/scoped.ts index 51ecfc04..3bd66731 100644 --- a/lib/scoped.ts +++ b/lib/scoped.ts @@ -1,7 +1,7 @@ import type { Operation } from "./types.ts"; import { trap } from "./task.ts"; -import { createScopeInternal } from "./scope.ts"; import { useCoroutine } from "./coroutine.ts"; +import { createScopeInternal } from "./scope-internal.ts"; /** * Encapsulate an operation so that no effects will persist outside of diff --git a/lib/spawn.ts b/lib/spawn.ts index 93df33ac..0ec1e5e4 100644 --- a/lib/spawn.ts +++ b/lib/spawn.ts @@ -1,8 +1,57 @@ import { Ok } from "./result.ts"; -import type { ScopeInternal } from "./scope.ts"; +import type { ScopeInternal } from "./scope-internal.ts"; import { createTask, type NewTask } from "./task.ts"; import type { Effect, Operation, Task } from "./types.ts"; +/** + * Run another operation concurrently as a child of the current one. + * + * The spawned operation will begin executing immediately and control will + * return to the caller when it reaches its first suspend + * + * @example + * ```js + * import { main, sleep, suspend, spawn } from 'effection'; + * + * await main(function*() { + * yield* spawn(function*() { + * yield* sleep(1000); + * console.log("hello"); + * }); + * yield* spawn(function*() { + * yield* sleep(2000); + * console.log("world"); + * }); + * yield* suspend(); + * }); + * ``` + * + * You should prefer using the spawn operation over calling + * {@link Scope.run} from within Effection code. The reason being that a + * synchronous failure in the spawned operation will not be caught + * until the next yield point when using `run`, which results in lines + * being executed that should not. + * + * @example + * ```js + * import { main, suspend, spawn, useScope } from 'effection'; + * + * await main(function*() { + * yield* useScope(); + * + * scope.run(function*() { + * throw new Error('boom!'); + * }); + * + * console.log('this code will run and probably should not'); + * + * yield* suspend(); // <- error is thrown after this. + * }); + * ``` + * @param operation the operation to run as a child of the current task + * @typeParam T the type that the spawned task evaluates to + * @returns a {@link Task} representing a handle to the running operation + */ export function* spawn(op: () => Operation): Operation> { let { task, start } = (yield Spawn(op)) as NewTask; start(); diff --git a/lib/task.ts b/lib/task.ts index fbd326b4..6f3b5b37 100644 --- a/lib/task.ts +++ b/lib/task.ts @@ -5,7 +5,7 @@ import { drain } from "./drain.ts"; import { lazyPromise, lazyPromiseWithResolvers } from "./lazy-promise.ts"; import { Just, type Maybe, Nothing } from "./maybe.ts"; import { Err, Ok, type Result, unbox } from "./result.ts"; -import { createScopeInternal, type ScopeInternal } from "./scope.ts"; +import { createScopeInternal, type ScopeInternal } from "./scope-internal.ts"; import type { Coroutine, Operation, Resolve, Scope, Task } from "./types.ts"; import { withResolvers } from "./with-resolvers.ts";