diff --git a/package-lock.json b/package-lock.json index c5c964b..cb18809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "proposal-async-context", "version": "1.0.0", "license": "CC0-1.0", - "dependencies": { - "prettier": "2.8.7" - }, "devDependencies": { "@esbuild-kit/cjs-loader": "2.4.1", "@esbuild-kit/esm-loader": "2.5.4", @@ -19,6 +16,7 @@ "@types/node": "18.11.18", "ecmarkup": "^16.1.1", "mocha": "10.2.0", + "prettier": "2.8.7", "typescript": "4.9.4" } }, @@ -2093,6 +2091,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, "bin": { "prettier": "bin-prettier.js" }, @@ -4229,7 +4228,8 @@ "prettier": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==" + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true }, "prex": { "version": "0.4.9", diff --git a/src/fork.ts b/src/fork.ts index 0b30195..9a3ef1a 100644 --- a/src/fork.ts +++ b/src/fork.ts @@ -1,5 +1,5 @@ import type { Mapping } from "./mapping"; -import type { AsyncContext } from "./index"; +import type { Variable } from "./variable"; /** * FrozenRevert holds a frozen Mapping that will be simply restored when the @@ -40,11 +40,11 @@ export class FrozenRevert { * clone to the prior state. */ export class Revert { - #key: AsyncContext; + #key: Variable; #has: boolean; #prev: T | undefined; - constructor(mapping: Mapping, key: AsyncContext) { + constructor(mapping: Mapping, key: Variable) { this.#key = key; this.#has = mapping.has(key); this.#prev = mapping.get(key); diff --git a/src/index.ts b/src/index.ts index ec5977b..2279b32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,8 @@ -import { Storage } from "./storage"; - -type AnyFunc = (this: T, ...args: any) => any; - -export class AsyncContext { - static wrap>(fn: F): F { - const snapshot = Storage.snapshot(); - - function wrap(this: ThisType, ...args: Parameters): ReturnType { - const head = Storage.switch(snapshot); - try { - return fn.apply(this, args); - } finally { - Storage.restore(head); - } - } - - return wrap as unknown as F; - } - - run>( - value: T, - fn: F, - ...args: Parameters - ): ReturnType { - const revert = Storage.set(this, value); - try { - return fn.apply(null, args); - } finally { - Storage.restore(revert); - } - } - - get(): T | undefined { - return Storage.get(this); - } -} +import { Snapshot } from "./snapshot"; +import { Variable } from "./variable"; + +export const AsyncContext = { + Snapshot, + Variable, +}; +export { Snapshot, Variable }; diff --git a/src/mapping.ts b/src/mapping.ts index bcd2dcf..f6a8417 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -1,39 +1,38 @@ -import type { AsyncContext } from "./index"; +import type { Variable } from "./variable"; /** - * Stores all AsyncContext data, and tracks whether any snapshots have been + * Stores all Variable data, and tracks whether any snapshots have been * taken of the current data. */ export class Mapping { - #data: Map, unknown> | null; + #data: Map, unknown>; /** * If a snapshot of this data is taken, then further modifications cannot be * made directly. Instead, set/delete will clone this Mapping and modify * _that_ instance. */ - #frozen: boolean; + #frozen = false; - constructor(data: Map, unknown> | null) { + constructor(data: Map, unknown>) { this.#data = data; - this.#frozen = data === null; } - has(key: AsyncContext): boolean { - return this.#data?.has(key) || false; + has(key: Variable): boolean { + return this.#data.has(key) || false; } - get(key: AsyncContext): T | undefined { - return this.#data?.get(key) as T | undefined; + get(key: Variable): T | undefined { + return this.#data.get(key) as T | undefined; } /** * Like the standard Map.p.set, except that we will allocate a new Mapping * instance if this instance is frozen. */ - set(key: AsyncContext, value: T): Mapping { + set(key: Variable, value: T): Mapping { const mapping = this.#fork(); - mapping.#data!.set(key, value); + mapping.#data.set(key, value); return mapping; } @@ -41,9 +40,9 @@ export class Mapping { * Like the standard Map.p.delete, except that we will allocate a new Mapping * instance if this instance is frozen. */ - delete(key: AsyncContext): Mapping { + delete(key: Variable): Mapping { const mapping = this.#fork(); - mapping.#data!.delete(key); + mapping.#data.delete(key); return mapping; } diff --git a/src/promise-polyfill.ts b/src/promise-polyfill.ts index 282109b..aa0d78b 100644 --- a/src/promise-polyfill.ts +++ b/src/promise-polyfill.ts @@ -1,12 +1,13 @@ import { AsyncContext } from "./index"; -type AnyFunc = (...args: any) => any; +import type { AnyFunc } from "./types"; export const nativeThen = Promise.prototype.then; +const { wrap } = AsyncContext.Snapshot; -function wrapFn(fn: F | null | undefined) { +function wrapFn>(fn: F | null | undefined) { if (typeof fn !== "function") return undefined; - return AsyncContext.wrap(fn); + return wrap(fn); } export function then( diff --git a/src/snapshot.ts b/src/snapshot.ts new file mode 100644 index 0000000..3edb175 --- /dev/null +++ b/src/snapshot.ts @@ -0,0 +1,36 @@ +import { Storage } from "./storage"; + +import type { FrozenRevert } from "./fork"; +import type { AnyFunc } from "./types"; + +export class Snapshot { + #snapshot = Storage.snapshot(); + + static wrap>(fn: F): F { + const snapshot = Storage.snapshot(); + + function wrap(this: ThisType, ...args: Parameters): ReturnType { + return run(fn, this, args, snapshot); + } + + return wrap as unknown as F; + } + + run>(fn: F, ...args: Parameters) { + return run(fn, null as any, args, this.#snapshot); + } +} + +function run>( + fn: F, + context: ThisType, + args: any[], + snapshot: FrozenRevert +): ReturnType { + const revert = Storage.switch(snapshot); + try { + return fn.apply(context, args); + } finally { + Storage.restore(revert); + } +} diff --git a/src/storage.ts b/src/storage.ts index d67fcb1..e415455 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,36 +1,44 @@ import { Mapping } from "./mapping"; import { FrozenRevert, Revert } from "./fork"; -import type { AsyncContext } from "./index"; + +import type { Variable } from "./variable"; /** * Storage is the (internal to the language) storage container of all - * AsyncContext data. + * Variable data. * - * None of the methods here are exposed to users, they're only exposed to the AsyncContext class. + * None of the methods here are exposed to users, they're only exposed internally. */ export class Storage { - static #current: Mapping = new Mapping(null); + static #current: Mapping = new Mapping(new Map()); + + /** + * Has checks if the Variable has a value. + */ + static has(key: Variable): boolean { + return this.#current.has(key); + } /** - * Get retrieves the current value assigned to the AsyncContext. + * Get retrieves the current value assigned to the Variable. */ - static get(key: AsyncContext): T | undefined { + static get(key: Variable): T | undefined { return this.#current.get(key); } /** - * Set assigns a new value to the AsyncContext, returning a revert that can + * Set assigns a new value to the Variable, returning a revert that can * undo the modification at a later time. */ - static set(key: AsyncContext, value: T): FrozenRevert | Revert { + static set(key: Variable, value: T): FrozenRevert | Revert { // If the Mappings are frozen (someone has snapshot it), then modifying the // mappings will return a clone containing the modification. const current = this.#current; - const undo = current.isFrozen() + const revert = current.isFrozen() ? new FrozenRevert(current) - : new Revert(current, key); + : new Revert(current, key); this.#current = this.#current.set(key, value); - return undo; + return revert; } /** diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..bb97783 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type AnyFunc = (this: T, ...args: any) => any; diff --git a/src/variable.ts b/src/variable.ts new file mode 100644 index 0000000..bb45b6c --- /dev/null +++ b/src/variable.ts @@ -0,0 +1,43 @@ +import { Storage } from "./storage"; + +import type { AnyFunc } from "./types"; + +export interface VariableOptions { + name?: string; + defaultValue?: T; +} + +export class Variable { + #name = ""; + #defaultValue: T | undefined; + + constructor(options?: VariableOptions) { + if (options) { + if ("name" in options) { + this.#name = String(options.name); + } + this.#defaultValue = options.defaultValue; + } + } + + get name() { + return this.#name; + } + + run>( + value: T, + fn: F, + ...args: Parameters + ): ReturnType { + const revert = Storage.set(this, value); + try { + return fn.apply(null, args); + } finally { + Storage.restore(revert); + } + } + + get(): T | undefined { + return Storage.has(this) ? Storage.get(this) : this.#defaultValue; + } +} diff --git a/tests/async-context.test.ts b/tests/async-context.test.ts index f4b5376..57a094c 100644 --- a/tests/async-context.test.ts +++ b/tests/async-context.test.ts @@ -16,11 +16,11 @@ function test(name: string, fn: () => void) { fn(); // Ensure we're running from a new state, which won't be frozen. - const throwaway = new AsyncContext(); + const throwaway = new AsyncContext.Variable(); throwaway.run(null, fn); throwaway.run(null, () => { - AsyncContext.wrap(() => {}); + AsyncContext.Snapshot.wrap(() => {}); // Ensure we're running from a new state, which is frozen. fn(); @@ -31,7 +31,7 @@ function test(name: string, fn: () => void) { describe("sync", () => { describe("run and get", () => { test("has initial undefined state", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const actual = ctx.get(); @@ -39,7 +39,7 @@ describe("sync", () => { }); test("return value", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const expected = { id: 1 }; const actual = ctx.run({ id: 2 }, () => expected); @@ -48,7 +48,7 @@ describe("sync", () => { }); test("get returns current context value", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const expected = { id: 1 }; ctx.run(expected, () => { @@ -57,7 +57,7 @@ describe("sync", () => { }); test("get within nesting contexts", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; @@ -72,8 +72,8 @@ describe("sync", () => { }); test("get within nesting different contexts", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; @@ -94,8 +94,8 @@ describe("sync", () => { describe("wrap", () => { test("stores initial undefined state", () => { - const ctx = new AsyncContext(); - const wrapped = AsyncContext.wrap(() => ctx.get()); + const ctx = new AsyncContext.Variable(); + const wrapped = AsyncContext.Snapshot.wrap(() => ctx.get()); ctx.run({ id: 1 }, () => { assert.equal(wrapped(), undefined); @@ -103,11 +103,11 @@ describe("sync", () => { }); test("stores current state", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const expected = { id: 1 }; const wrap = ctx.run(expected, () => { - const wrap = AsyncContext.wrap(() => ctx.get()); + const wrap = AsyncContext.Snapshot.wrap(() => ctx.get()); assert.equal(wrap(), expected); assert.equal(ctx.get(), expected); return wrap; @@ -118,12 +118,12 @@ describe("sync", () => { }); test("runs within wrap", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const [wrap1, wrap2] = ctx.run(first, () => { - const wrap1 = AsyncContext.wrap(() => { + const wrap1 = AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), first); ctx.run(second, () => { @@ -138,7 +138,7 @@ describe("sync", () => { assert.equal(ctx.get(), second); }); - const wrap2 = AsyncContext.wrap(() => { + const wrap2 = AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), first); ctx.run(second, () => { @@ -157,12 +157,12 @@ describe("sync", () => { }); test("runs within wrap", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const [wrap1, wrap2] = ctx.run(first, () => { - const wrap1 = AsyncContext.wrap(() => { + const wrap1 = AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), first); ctx.run(second, () => { @@ -177,7 +177,7 @@ describe("sync", () => { assert.equal(ctx.get(), second); }); - const wrap2 = AsyncContext.wrap(() => { + const wrap2 = AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), first); ctx.run(second, () => { @@ -196,13 +196,13 @@ describe("sync", () => { }); test("runs different context within wrap", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const [wrap1, wrap2] = a.run(first, () => { - const wrap1 = AsyncContext.wrap(() => { + const wrap1 = AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), undefined); @@ -217,7 +217,7 @@ describe("sync", () => { a.run(second, () => {}); - const wrap2 = AsyncContext.wrap(() => { + const wrap2 = AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), undefined); @@ -242,13 +242,13 @@ describe("sync", () => { }); test("runs different context within wrap, 2", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const [wrap1, wrap2] = a.run(first, () => { - const wrap1 = AsyncContext.wrap(() => { + const wrap1 = AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), undefined); @@ -263,7 +263,7 @@ describe("sync", () => { b.run(second, () => {}); - const wrap2 = AsyncContext.wrap(() => { + const wrap2 = AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), undefined); @@ -288,18 +288,18 @@ describe("sync", () => { }); test("wrap within nesting contexts", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const [firstWrap, secondWrap] = ctx.run(first, () => { - const firstWrap = AsyncContext.wrap(() => { + const firstWrap = AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), first); }); firstWrap(); const secondWrap = ctx.run(second, () => { - const secondWrap = AsyncContext.wrap(() => { + const secondWrap = AsyncContext.Snapshot.wrap(() => { firstWrap(); assert.equal(ctx.get(), second); }); @@ -323,20 +323,20 @@ describe("sync", () => { }); test("wrap within nesting different contexts", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const [firstWrap, secondWrap] = a.run(first, () => { - const firstWrap = AsyncContext.wrap(() => { + const firstWrap = AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), undefined); }); firstWrap(); const secondWrap = b.run(second, () => { - const secondWrap = AsyncContext.wrap(() => { + const secondWrap = AsyncContext.Snapshot.wrap(() => { firstWrap(); assert.equal(a.get(), first); assert.equal(b.get(), second); @@ -365,9 +365,9 @@ describe("sync", () => { }); test("wrap within nesting different contexts, 2", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); - const c = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); + const c = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const third = { id: 3 }; @@ -375,7 +375,7 @@ describe("sync", () => { const wrap = a.run(first, () => { const wrap = b.run(second, () => { const wrap = c.run(third, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), second); assert.equal(c.get(), third); @@ -403,19 +403,19 @@ describe("sync", () => { }); test("wrap within nesting different contexts, 3", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); - const c = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); + const c = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const third = { id: 3 }; const wrap = a.run(first, () => { const wrap = b.run(second, () => { - AsyncContext.wrap(() => {}); + AsyncContext.Snapshot.wrap(() => {}); const wrap = c.run(third, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), second); assert.equal(c.get(), third); @@ -443,19 +443,19 @@ describe("sync", () => { }); test("wrap within nesting different contexts, 4", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); - const c = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); + const c = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const third = { id: 3 }; const wrap = a.run(first, () => { - AsyncContext.wrap(() => {}); + AsyncContext.Snapshot.wrap(() => {}); const wrap = b.run(second, () => { const wrap = c.run(third, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), second); assert.equal(c.get(), third); @@ -483,9 +483,9 @@ describe("sync", () => { }); test("wrap within nesting different contexts, 5", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); - const c = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); + const c = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const third = { id: 3 }; @@ -493,14 +493,14 @@ describe("sync", () => { const wrap = a.run(first, () => { const wrap = b.run(second, () => { const wrap = c.run(third, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), second); assert.equal(c.get(), third); }); }); - AsyncContext.wrap(() => {}); + AsyncContext.Snapshot.wrap(() => {}); assert.equal(a.get(), first); assert.equal(b.get(), second); @@ -524,9 +524,9 @@ describe("sync", () => { }); test("wrap within nesting different contexts, 6", () => { - const a = new AsyncContext(); - const b = new AsyncContext(); - const c = new AsyncContext(); + const a = new AsyncContext.Variable(); + const b = new AsyncContext.Variable(); + const c = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const third = { id: 3 }; @@ -534,7 +534,7 @@ describe("sync", () => { const wrap = a.run(first, () => { const wrap = b.run(second, () => { const wrap = c.run(third, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(a.get(), first); assert.equal(b.get(), second); assert.equal(c.get(), third); @@ -546,7 +546,7 @@ describe("sync", () => { return wrap; }); - AsyncContext.wrap(() => {}); + AsyncContext.Snapshot.wrap(() => {}); assert.equal(a.get(), first); assert.equal(b.get(), undefined); @@ -565,17 +565,17 @@ describe("sync", () => { }); test("wrap out of order", () => { - const ctx = new AsyncContext(); + const ctx = new AsyncContext.Variable(); const first = { id: 1 }; const second = { id: 2 }; const firstWrap = ctx.run(first, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), first); }); }); const secondWrap = ctx.run(second, () => { - return AsyncContext.wrap(() => { + return AsyncContext.Snapshot.wrap(() => { assert.equal(ctx.get(), second); }); });