-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #933 from thefrontside/scoped
✨Add `scoped()` higher order operation
- Loading branch information
Showing
3 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type { Operation } from "./types.ts"; | ||
import { trap } from "./task.ts"; | ||
import { createScopeInternal } from "./scope.ts"; | ||
import { useCoroutine } from "./coroutine.ts"; | ||
|
||
/** | ||
* Encapsulate an operation so that no effects will persist outside of | ||
* it. All active effects such as concurrent tasks and resources will be | ||
* shut down, and all contexts will be restored to their values outside | ||
* of the scope. | ||
* | ||
* @param operation - the operation to be encapsulated | ||
* | ||
* @returns the scoped operation | ||
*/ | ||
export function scoped<T>(operation: () => Operation<T>): Operation<T> { | ||
return { | ||
*[Symbol.iterator]() { | ||
let routine = yield* useCoroutine(); | ||
let original = routine.scope; | ||
let [scope, destroy] = createScopeInternal(original); | ||
try { | ||
routine.scope = scope; | ||
return yield* trap(operation); | ||
} finally { | ||
routine.scope = original; | ||
yield* destroy(); | ||
} | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { | ||
createContext, | ||
resource, | ||
run, | ||
scoped, | ||
sleep, | ||
spawn, | ||
suspend, | ||
} from "../mod.ts"; | ||
import { describe, expect, it } from "./suite.ts"; | ||
|
||
describe("scoped", () => { | ||
describe("task", () => { | ||
it("shuts down after completion", () => | ||
run(function* () { | ||
let didEnter = false; | ||
let didExit = false; | ||
|
||
yield* scoped(function* () { | ||
yield* spawn(function* () { | ||
try { | ||
didEnter = true; | ||
yield* suspend(); | ||
} finally { | ||
didExit = true; | ||
} | ||
}); | ||
yield* sleep(0); | ||
}); | ||
|
||
expect(didEnter).toBe(true); | ||
expect(didExit).toBe(true); | ||
})); | ||
|
||
it("shuts down after error", () => | ||
run(function* () { | ||
let didEnter = false; | ||
let didExit = false; | ||
|
||
try { | ||
yield* scoped(function* () { | ||
yield* spawn(function* () { | ||
try { | ||
didEnter = true; | ||
yield* suspend(); | ||
} finally { | ||
didExit = true; | ||
} | ||
}); | ||
yield* sleep(0); | ||
throw new Error("boom!"); | ||
}); | ||
} catch (error) { | ||
expect(error).toMatchObject({ message: "boom!" }); | ||
expect(didEnter).toBe(true); | ||
expect(didExit).toBe(true); | ||
} | ||
})); | ||
|
||
it("delimits error boundaries", () => | ||
run(function* () { | ||
try { | ||
yield* scoped(function* () { | ||
yield* spawn(function* () { | ||
throw new Error("boom!"); | ||
}); | ||
yield* suspend(); | ||
}); | ||
} catch (error) { | ||
expect(error).toMatchObject({ message: "boom!" }); | ||
} | ||
})); | ||
}); | ||
describe("resource", () => { | ||
it("shuts down after completion", () => | ||
run(function* () { | ||
let status = "pending"; | ||
yield* scoped(function* () { | ||
yield* resource<void>(function* (provide) { | ||
try { | ||
status = "open"; | ||
yield* provide(); | ||
} finally { | ||
status = "closed"; | ||
} | ||
}); | ||
yield* sleep(0); | ||
expect(status).toEqual("open"); | ||
}); | ||
expect(status).toEqual("closed"); | ||
})); | ||
|
||
it("shuts down after error", () => | ||
run(function* () { | ||
let status = "pending"; | ||
try { | ||
yield* scoped(function* () { | ||
yield* resource<void>(function* (provide) { | ||
try { | ||
status = "open"; | ||
yield* provide(); | ||
} finally { | ||
status = "closed"; | ||
} | ||
}); | ||
yield* sleep(0); | ||
expect(status).toEqual("open"); | ||
throw new Error("boom!"); | ||
}); | ||
} catch (error) { | ||
expect((error as Error).message).toEqual("boom!"); | ||
expect(status).toEqual("closed"); | ||
} | ||
})); | ||
|
||
it("delimits error boundaries", () => | ||
run(function* () { | ||
try { | ||
yield* scoped(function* () { | ||
yield* resource<void>(function* (provide) { | ||
yield* spawn(function* () { | ||
yield* sleep(0); | ||
throw new Error("boom!"); | ||
}); | ||
yield* provide(); | ||
}); | ||
yield* suspend(); | ||
}); | ||
} catch (error) { | ||
expect(error).toMatchObject({ message: "boom!" }); | ||
} | ||
})); | ||
}); | ||
describe("context", () => { | ||
let context = createContext<string>("greetting", "hi"); | ||
it("is restored after exiting scope", () => | ||
run(function* () { | ||
yield* scoped(function* () { | ||
yield* context.set("hola"); | ||
}); | ||
expect(yield* context.get()).toEqual("hi"); | ||
})); | ||
|
||
it("is restored after erroring", () => | ||
run(function* () { | ||
try { | ||
yield* scoped(function* () { | ||
yield* context.set("hola"); | ||
throw new Error("boom!"); | ||
}); | ||
} catch (error) { | ||
expect(error).toMatchObject({ message: "boom!" }); | ||
} finally { | ||
expect(yield* context.get()).toEqual("hi"); | ||
} | ||
})); | ||
}); | ||
}); |