diff --git a/deno.json b/deno.json
index aee886609..afa23d842 100644
--- a/deno.json
+++ b/deno.json
@@ -10,7 +10,7 @@
"build:jsr": "deno run -A tasks/build-jsr.ts"
},
"lint": {
- "rules": { "exclude": ["prefer-const", "require-yield"] },
+ "rules": { "exclude": ["prefer-const", "require-yield", "ban-types"] },
"exclude": ["build", "docs", "tasks", "bench"],
"plugins": ["lint/prefer-let.ts"]
},
diff --git a/experimental.ts b/experimental.ts
index e69de29bb..7f7317fb3 100644
--- a/experimental.ts
+++ b/experimental.ts
@@ -0,0 +1 @@
+export * from "./lib/api.ts";
diff --git a/lib/api-internal.ts b/lib/api-internal.ts
new file mode 100644
index 000000000..b49d5e808
--- /dev/null
+++ b/lib/api-internal.ts
@@ -0,0 +1,220 @@
+// deno-lint-ignore-file ban-types no-explicit-any
+import { createContext } from "./context.ts";
+import type {
+ Api,
+ Around,
+ Context,
+ Effect,
+ Middleware,
+ Operation,
+ Scope,
+} from "./types.ts";
+import { Children } from "./contexts.ts";
+import { Ok } from "./result.ts";
+
+export interface ApiInternal extends Api {
+ context: Context<{
+ local: Decorator;
+ total: Decorator;
+ handle: A;
+ }>;
+ core: A;
+}
+
+export function createApiInternal(
+ name: string,
+ core: A,
+): ApiInternal {
+ let fields = Object.keys(core) as (keyof A)[];
+
+ let context = createContext(`api::${name}`) as ApiInternal["context"];
+
+ let api: ApiInternal = {
+ core,
+ context,
+
+ invoke: (scope, key, args) => {
+ let handle = scope.get(api.context)?.handle ?? core;
+
+ let member = handle[key];
+ if (typeof member === "function") {
+ return member(...args);
+ } else {
+ return member;
+ }
+ },
+ around: (decorator, options = { at: "max" }) => ({
+ *[Symbol.iterator]() {
+ let scope = (yield GetScope) as Scope;
+ decorateApi(scope, api, decorator, options);
+ },
+ }),
+ operations: fields.reduce((sum, field) => {
+ if (typeof core[field] === "function") {
+ return Object.assign(sum, {
+ [field]: (...args: any) => ({
+ *[Symbol.iterator]() {
+ let scope = (yield GetScope) as Scope;
+ let target = api.invoke(scope, field, args);
+
+ return isOperation(target) ? yield* target : target;
+ },
+ }),
+ });
+ } else {
+ return Object.assign(sum, {
+ [field]: {
+ *[Symbol.iterator]() {
+ let scope = (yield GetScope) as Scope;
+ let target = api.invoke(scope, field, [] as any);
+ return isOperation(target) ? yield* target : target;
+ },
+ },
+ });
+ }
+ }, {} as Api["operations"]),
+ };
+ return api;
+}
+
+export function decorateApi(
+ scope: Scope,
+ api: ApiInternal,
+ decorator: Partial>,
+ options?: {
+ at: "min" | "max";
+ },
+) {
+ // read existing total and local
+ let current = scope.get(api.context) ?? { total: {}, local: {} };
+
+ let local = decorate(current.local, {
+ [options?.at ?? "max"]: decorator,
+ });
+
+ if (!scope.hasOwn(api.context)) {
+ scope.set(api.context, {
+ local,
+ total: current.total,
+ handle: api.core,
+ });
+ } else {
+ current.local = local;
+ }
+
+ install(scope, api, current.total);
+}
+
+function decorate(base: Decorator, next: Decorator): Decorator {
+ return {
+ max: base.max ? next.max ? append(base.max, next.max) : base.max : next.max,
+ min: next.min ? base.min ? append(next.min, base.min) : next.min : base.min,
+ };
+}
+
+function append(
+ outer: Partial>,
+ inner: Partial>,
+): Partial> {
+ let result: Partial> = { ...outer };
+ for (let key of Object.keys(inner) as (keyof A)[]) {
+ let current = outer[key];
+ let decoration = inner[key];
+ if (!current) {
+ result[key] = decoration;
+ } else {
+ let pair = [current, decoration] as Middleware[];
+ result[key] = combine(pair) as Around[keyof A];
+ }
+ }
+ return result;
+}
+
+function install(
+ scope: Scope,
+ api: ApiInternal,
+ total: Decorator,
+): void {
+ if (scope.hasOwn(api.context)) {
+ let context = scope.expect(api.context);
+
+ context.total = total;
+
+ total = decorate(context.total, context.local);
+
+ context.handle = createApiHandle(total, api.core);
+ }
+
+ for (let child of scope.expect(Children)) {
+ install(child, api, total);
+ }
+}
+
+function createApiHandle(decoration: Decorator, core: A): A {
+ let around = decoration.max
+ ? decoration.min ? append(decoration.max, decoration.min) : decoration.max
+ : decoration.min;
+ if (!around) {
+ return core;
+ }
+ let handle: A = {} as A;
+ for (let key of Object.keys(core as object) as Array) {
+ let middleware = around[key] as Middleware | undefined;
+ if (middleware) {
+ if (typeof core[key] === "function") {
+ handle[key] = ((...args: unknown[]) =>
+ middleware(args, core[key] as (...args: unknown[]) => unknown)) as A[
+ keyof A
+ ];
+ } else {
+ Object.defineProperty(handle, key, {
+ enumerable: true,
+ get() {
+ return middleware([], () => core[key]);
+ },
+ });
+ }
+ } else {
+ handle[key] = 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))
+ );
+}
+
+interface Decorator {
+ min?: Partial>;
+ max?: Partial>;
+}
+
+const GetScope: Effect = {
+ description: "Fast, non-typesafe lookup of co-routine scope",
+ enter: (resolve, routine) => {
+ resolve(Ok(routine.scope));
+ return (didExit) => didExit(Ok());
+ },
+};
diff --git a/lib/api.ts b/lib/api.ts
new file mode 100644
index 000000000..3e3f98a6a
--- /dev/null
+++ b/lib/api.ts
@@ -0,0 +1,36 @@
+// deno-lint-ignore-file ban-types
+import type { Api, Context, Operation, Scope } from "./types.ts";
+import { createApiInternal } from "./api-internal.ts";
+import type { ScopeInternal } from "./scope-internal.ts";
+
+export function createApi(name: string, core: A): Api {
+ return createApiInternal(name, core);
+}
+
+interface ScopeApi {
+ create(parent: Scope): [Scope, () => Operation];
+ destroy(scope: Scope): Operation;
+ set(scope: Scope, context: Context, value: T): T;
+ delete(scope: Scope, context: Context): boolean;
+}
+
+interface Apis {
+ Scope: Api;
+}
+
+export const api: Apis = {
+ Scope: createApi("Scope", {
+ create() {
+ throw new TypeError(`no handler for Scope.create()`);
+ },
+ destroy(scope) {
+ return (scope as ScopeInternal).destroy();
+ },
+ set(scope, context, value) {
+ return (scope as ScopeInternal).contexts[context.name] = value;
+ },
+ delete(scope, context) {
+ return delete (scope as ScopeInternal).contexts[context.name];
+ },
+ }),
+};
diff --git a/lib/scope-internal.ts b/lib/scope-internal.ts
index 9351d0a81..72d760d0b 100644
--- a/lib/scope-internal.ts
+++ b/lib/scope-internal.ts
@@ -1,3 +1,5 @@
+import { type ApiInternal, decorateApi } from "./api-internal.ts";
+import { api as effection } from "./api.ts";
import { Children, Priority } from "./contexts.ts";
import { createFuture } from "./future.ts";
import { Err, Ok, unbox } from "./result.ts";
@@ -5,11 +7,36 @@ import { createTask } from "./task.ts";
import type { Context, Operation, Scope, Task } from "./types.ts";
+const api = effection.Scope;
+
export function createScopeInternal(
parent?: Scope,
+): [ScopeInternal, () => Operation] {
+ if (!parent) {
+ let [global, destroy] = buildScopeInternal();
+ global.around(api, {
+ create([parent]) {
+ return buildScopeInternal(parent);
+ },
+ }, { at: "min" });
+ return [global, destroy] as const;
+ } else {
+ return api.invoke(parent, "create", [parent]) as [
+ ScopeInternal,
+ () => Operation,
+ ];
+ }
+}
+
+export function buildScopeInternal(
+ parent?: Scope,
): [ScopeInternal, () => Operation] {
let destructors = new Set<() => Operation>();
let destruction = createFuture();
+ let signaled = false;
+ let unbind = parent
+ ? (parent as ScopeInternal).ensure(() => destroy())
+ : () => {};
let contexts: Record = Object.create(
parent ? (parent as ScopeInternal).contexts : null,
@@ -21,7 +48,7 @@ export function createScopeInternal(
return (contexts[context.name] ?? context.defaultValue) as T | undefined;
},
set(context: Context, value: T): T {
- return contexts[context.name] = value;
+ return api.invoke(scope, "set", [scope, context, value]) as T;
},
expect(context: Context): T {
let value = scope.get(context);
@@ -33,7 +60,7 @@ export function createScopeInternal(
return value;
},
delete(context: Context): boolean {
- return delete contexts[context.name];
+ return api.invoke(scope, "delete", [scope, context]);
},
hasOwn(context: Context): boolean {
return !!Reflect.getOwnPropertyDescriptor(contexts, context.name);
@@ -48,54 +75,61 @@ export function createScopeInternal(
},
};
},
+ around(
+ api: ApiInternal,
+ ...params: Parameters["around"]>
+ ) {
+ decorateApi(scope, api, ...params);
+ },
ensure(op: () => Operation): () => void {
destructors.add(op);
return () => destructors.delete(op);
},
- });
-
- scope.set(Priority, scope.expect(Priority) + 1);
- scope.set(Children, new Set());
- parent?.expect(Children).add(scope);
- let destroy = function* (): Operation {
- destroy = () => destruction.future;
-
- parent?.expect(Children).delete(scope);
- unbind();
- let outcome = Ok();
- try {
- while (destructors.size > 0) {
- let current = [...destructors];
- destructors.clear();
- for (let destructor of current) {
- try {
- yield* destructor();
- } catch (error) {
- outcome = Err(error as Error);
+ *destroy(): Operation {
+ if (signaled) {
+ return yield* destruction.future;
+ }
+ signaled = true;
+ parent?.expect(Children).delete(scope);
+ unbind();
+ let outcome = Ok();
+ try {
+ while (destructors.size > 0) {
+ let current = [...destructors];
+ destructors.clear();
+ for (let destructor of current) {
+ try {
+ yield* destructor();
+ } catch (error) {
+ outcome = Err(error);
+ }
}
}
+ } finally {
+ if (outcome.ok) {
+ destruction.resolve();
+ } else {
+ destruction.reject(outcome.error);
+ }
}
- } finally {
- if (outcome.ok) {
- destruction.resolve();
- } else {
- destruction.reject(outcome.error);
- }
- }
- unbox(outcome);
- };
+ unbox(outcome);
+ },
+ });
- let unbind = parent
- ? (parent as ScopeInternal).ensure(() => destroy())
- : () => {};
+ scope.set(Priority, scope.expect(Priority) + 1);
+ scope.set(Children, new Set());
+ parent?.expect(Children).add(scope);
+
+ let destroy = () => api.invoke(scope, "destroy", [scope]);
- return [scope, () => destroy()];
+ return [scope, destroy];
}
export interface ScopeInternal extends Scope, AsyncDisposable {
contexts: Record;
ensure(op: () => Operation): () => void;
+ destroy(): Operation;
}
diff --git a/lib/types.ts b/lib/types.ts
index 816ac7adf..434a4e36d 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,3 +1,4 @@
+// deno-lint-ignore-file no-explicit-any
import type { Maybe } from "./maybe.ts";
import type { Result } from "./result.ts";
@@ -336,8 +337,98 @@ 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;
+
+ /**
+ * Enhance an {@link Api} within this scope by surrounding it with
+ * middleware.
+ *
+ * @param api - the api being enhanced
+ * @param middlewares - collection of {@link Middleware} to be added to this {@link Api}
+ * @param options - specifies which layer of dispatch `middlewares` will be applied
+ * @see {@link Api.around}
+ * @since 4.1
+ */
+ around(api: Api, ...options: Parameters["around"]>): void;
}
+/**
+ * A set of methods and values that can be decorated on a per-scope
+ * basis. Apis are ideal for situations that require context
+ * sensitivity such as dependency injection, test mocking, and
+ * instrumentation.
+ *
+ * @template A - core shape of the Api
+ * @see {@link createApi}
+ * @since 4.1
+ */
+export interface Api {
+ /**
+ * Every member of `A` "lifted" into an operation that invokes that
+ * member on the current {Scope}
+ */
+ 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;
+ };
+ /**
+ * Enhance an {@link Api} within this scope by surrounding it with
+ * middleware.
+ *
+ * @param middlewares - a set of decorators that will surround the api core
+ * @param options - specify at which layer of dispatch, `middleware` will apply
+ * @returns an {Operation} that installs the middleware in the current {Scope}
+ */
+ around: (
+ middlewares: Partial>,
+ options?: {
+ at: "min" | "max";
+ },
+ ) => Operation;
+
+ /**
+ * Call an API as it exists on `scope`.
+ */
+ invoke: (
+ scope: Scope,
+ key: K,
+ args: A[K] extends (...args: any) => unknown ? Parameters : [],
+ ) => A[K] extends (...args: any) => unknown ? ReturnType : A[K];
+}
+
+/**
+ * An general function that can be used to surround any other function
+ * or value.
+ *
+ * @since 4.1
+ */
+export interface Middleware {
+ /**
+ * Execute a single link in the middleware stack by doing whatever
+ * computation is necessary and then optionally delegating to the
+ * next link.
+ *
+ * @param args - the arguments to the value being surrounded.
+ * @param next - the next function in the change. It will accept the
+ * arguments contained in `args`
+ * @returns a value with the same shape as `next()`'s return value
+ */
+ (args: TArgs, next: (...args: TArgs) => TReturn): TReturn;
+}
+
+/**
+ * The shape of middlewares can surround a particular {Api}
+ *
+ * Members of an Api that are values are surrounded by no-arg functions.
+ */
+export type Around = {
+ [K in keyof Api]: Api[K] extends (...args: infer TArgs) => infer TReturn
+ ? Middleware
+ : Middleware<[], Api[K]>;
+};
+
/**
* Unwrap the type of an `Operation`.
* Analogous to the built in [`Awaited`](https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype) type.
diff --git a/test/api.test.ts b/test/api.test.ts
new file mode 100644
index 000000000..6e9083445
--- /dev/null
+++ b/test/api.test.ts
@@ -0,0 +1,425 @@
+import { run } from "../mod.ts";
+import { createApi } from "../experimental.ts";
+import { constant } from "../lib/constant.ts";
+import {
+ type Operation,
+ resource,
+ scoped,
+ spawn,
+ useScope,
+} from "../lib/mod.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.around({
+ 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.around({
+ *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.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("max1"));
+ return output.concat("/max1");
+ },
+ });
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("max2"));
+ return output.concat("/max2");
+ },
+ });
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("min1"));
+ return output.concat("/min1");
+ },
+ });
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("min2"));
+ return output.concat("/min2");
+ },
+ });
+
+ 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;
+ },
+ } as const,
+ );
+
+ await run(function* outer() {
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("outermax"));
+ return output.concat("/outermax");
+ },
+ });
+ yield* api.around({
+ *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.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("innermax"));
+ return output.concat("/innermax");
+ },
+ });
+ yield* api.around({
+ *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",
+ ]);
+ });
+ });
+
+ it("propagates new ancestor middleware to existing child scopes", async () => {
+ let api = createApi("test", {
+ *num(value: number): Operation {
+ return value;
+ },
+ });
+
+ await run(function* () {
+ let nummer = yield* resource<{ num(): Operation }>(
+ function* (provide) {
+ let scope = yield* useScope();
+ yield* provide({
+ *num() {
+ return yield* scope.run(() => api.operations.num(5));
+ },
+ });
+ },
+ );
+
+ yield* api.around({
+ *num(args, next) {
+ return (yield* next(...args)) * 2;
+ },
+ });
+
+ expect(yield* nummer.num()).toEqual(10);
+ });
+ });
+
+ it("propagates new ancestor middleware to existing child scopes that also have middleware", async () => {
+ let api = createApi("test", {
+ *test(order: string[]): Operation {
+ return order;
+ },
+ });
+
+ await run(function* () {
+ let tester = yield* resource<{ test(): Operation }>(
+ function* (provide) {
+ let scope = yield* useScope();
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("child"));
+ return output.concat("/child");
+ },
+ });
+ yield* provide({
+ *test() {
+ return yield* scope.run(() => api.operations.test([]));
+ },
+ });
+ },
+ );
+
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ let output = yield* next(input.concat("parent"));
+ return output.concat("/parent");
+ },
+ });
+
+ expect(yield* tester.test()).toEqual([
+ "parent",
+ "child",
+ "/child",
+ "/parent",
+ ]);
+ });
+ });
+
+ it("isolates sibling scopes from each other's middleware", async () => {
+ let api = createApi("test", {
+ *test(order: string[]): Operation {
+ return order;
+ },
+ });
+
+ function sibling(label: string) {
+ return resource<{ test(): Operation }>(function* (provide) {
+ let scope = yield* useScope();
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ return (yield* next(input.concat(label))).concat(`/${label}`);
+ },
+ });
+ yield* provide({
+ *test() {
+ return yield* scope.run(() => api.operations.test([]));
+ },
+ });
+ });
+ }
+
+ await run(function* () {
+ let a = yield* sibling("a");
+ let b = yield* sibling("b");
+
+ expect(yield* a.test()).toEqual(["a", "/a"]);
+ expect(yield* b.test()).toEqual(["b", "/b"]);
+ expect(yield* api.operations.test([])).toEqual([]);
+ });
+ });
+
+ it("composes middleware across a three-level scope tree", async () => {
+ let api = createApi("test", {
+ *test(order: string[]): Operation {
+ return order;
+ },
+ });
+
+ function layer(label: string) {
+ return resource<{ test(): Operation }>(function* (provide) {
+ let scope = yield* useScope();
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ return (yield* next(input.concat(label))).concat(`/${label}`);
+ },
+ });
+ yield* provide({
+ *test() {
+ return yield* scope.run(() => api.operations.test([]));
+ },
+ });
+ });
+ }
+
+ await run(function* () {
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ return (yield* next(input.concat("grand"))).concat("/grand");
+ },
+ });
+
+ let leaf = yield* resource<{ test(): Operation }>(
+ function* (provide) {
+ yield* api.around({
+ *test(args, next) {
+ let [input] = args;
+ return (yield* next(input.concat("parent"))).concat("/parent");
+ },
+ });
+ let child = yield* layer("child");
+ yield* provide(child);
+ },
+ );
+
+ expect(yield* leaf.test()).toEqual([
+ "grand",
+ "parent",
+ "child",
+ "/child",
+ "/parent",
+ "/grand",
+ ]);
+ });
+ });
+
+ it("does not affect un-aroundified keys when middleware is installed for a different key", async () => {
+ let api = createApi("test", {
+ *foo(): Operation {
+ return 1;
+ },
+ *bar(): Operation {
+ return 1;
+ },
+ });
+
+ await run(function* () {
+ yield* api.around({
+ *foo(args, next) {
+ return (yield* next(...args)) * 100;
+ },
+ });
+
+ expect(yield* api.operations.foo()).toEqual(100);
+ expect(yield* api.operations.bar()).toEqual(1);
+ });
+ });
+
+ it("does not leak middleware from a finished child scope back to its parent", async () => {
+ let api = createApi("test", {
+ *num(value: number): Operation {
+ return value;
+ },
+ });
+
+ await run(function* () {
+ let result = yield* scoped(function* () {
+ yield* api.around({
+ *num(args, next) {
+ return (yield* next(...args)) * 2;
+ },
+ });
+ return yield* api.operations.num(5);
+ });
+
+ expect(result).toEqual(10);
+ expect(yield* api.operations.num(5)).toEqual(5);
+ });
+ });
+});