Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
📝 document scope, spawn
Browse files Browse the repository at this point in the history
Spiff up the scope documentation. The `ScopeInternal` class was moved
to its own file so that it can be imported internally, but is not
exported from the main module.
cowboyd committed Jan 9, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 428bdde commit 5b0c0e7
Showing 5 changed files with 185 additions and 101 deletions.
86 changes: 86 additions & 0 deletions lib/scope-internal.ts
Original file line number Diff line number Diff line change
@@ -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<void>] {
let destructors = new Set<() => Operation<void>>();

let contexts: Record<string, unknown> = Object.create(
parent ? (parent as ScopeInternal).contexts : null,
);
let scope: ScopeInternal = Object.create({
[Symbol.toStringTag]: "Scope",
contexts,
get<T>(context: Context<T>): T | undefined {
return (contexts[context.name] ?? context.defaultValue) as T | undefined;
},
set<T>(context: Context<T>, value: T): T {
return contexts[context.name] = value;
},
expect<T>(context: Context<T>): T {
let value = scope.get(context);
if (typeof value === "undefined") {
let error = new Error(context.name);
error.name = `MissingContextError`;
throw error;
}
return value;
},
delete<T>(context: Context<T>): boolean {
return delete contexts[context.name];
},
hasOwn<T>(context: Context<T>): boolean {
return !!Reflect.getOwnPropertyDescriptor(contexts, context.name);
},
run<T>(operation: () => Operation<T>): Task<T> {
let { task, start } = createTask({ operation, owner: scope });
start();
return task;
},
spawn<T>(operation: () => Operation<T>): Operation<Task<T>> {
return {
*[Symbol.iterator]() {
let { task, start } = createTask({ operation, owner: scope });
start();
return task;
},
};
},

ensure(op: () => Operation<void>): () => 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<void> {
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<string, unknown>;
ensure(op: () => Operation<void>): () => void;
}
145 changes: 47 additions & 98 deletions lib/scope.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,57 @@
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<void>] = 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()[0] as Scope;


/**
* 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<void>] {
let [scope, destroy] = createScopeInternal(parent);
return [scope, () => parent.run(destroy)];
}

export function createScopeInternal(
parent?: Scope,
): [ScopeInternal, () => Operation<void>] {
let destructors = new Set<() => Operation<void>>();

let contexts: Record<string, unknown> = Object.create(
parent ? (parent as ScopeInternal).contexts : null,
);
let scope: ScopeInternal = Object.create({
[Symbol.toStringTag]: "Scope",
contexts,
get<T>(context: Context<T>): T | undefined {
return (contexts[context.name] ?? context.defaultValue) as T | undefined;
},
set<T>(context: Context<T>, value: T): T {
return contexts[context.name] = value;
},
expect<T>(context: Context<T>): T {
let value = scope.get(context);
if (typeof value === "undefined") {
let error = new Error(context.name);
error.name = `MissingContextError`;
throw error;
}
return value;
},
delete<T>(context: Context<T>): boolean {
return delete contexts[context.name];
},
hasOwn<T>(context: Context<T>): boolean {
return !!Reflect.getOwnPropertyDescriptor(contexts, context.name);
},
run<T>(operation: () => Operation<T>): Task<T> {
let { task, start } = createTask({ operation, owner: scope });
start();
return task;
},
spawn<T>(operation: () => Operation<T>): Operation<Task<T>> {
return {
*[Symbol.iterator]() {
let { task, start } = createTask({ operation, owner: scope });
start();
return task;
},
};
},

ensure(op: () => Operation<void>): () => 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<void> {
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<string, unknown>;
ensure(op: () => Operation<void>): () => void;
}

/**
* Get the scope of the currently running {@link Operation}.
*
* @returns an operation yielding the current scope
*/
export function* useScope(): Operation<Scope> {
return (yield {
description: `useScope()`,
2 changes: 1 addition & 1 deletion lib/scoped.ts
Original file line number Diff line number Diff line change
@@ -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
51 changes: 50 additions & 1 deletion lib/spawn.ts
Original file line number Diff line number Diff line change
@@ -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<T>(op: () => Operation<T>): Operation<Task<T>> {
let { task, start } = (yield Spawn(op)) as NewTask<T>;
start();
2 changes: 1 addition & 1 deletion lib/task.ts
Original file line number Diff line number Diff line change
@@ -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";

0 comments on commit 5b0c0e7

Please sign in to comment.