diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..56b3bfc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + name: "Test" + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm test + + lint: + name: "Lint" + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run lint diff --git a/package-lock.json b/package-lock.json index 94f6a94..5faa9f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "@types/mocha": "10.0.1", "@types/node": "18.11.18", "ecmarkup": "^3.1.1", - "mocha": "10.2.0" + "mocha": "10.2.0", + "typescript": "4.9.4" } }, "node_modules/@babel/code-frame": { @@ -3449,6 +3450,19 @@ "node": ">=8" } }, + "node_modules/typescript": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", @@ -6288,6 +6302,12 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "typescript": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "dev": true + }, "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", diff --git a/package.json b/package.json index e89f3bd..09b3fe8 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "Async Context proposal for JavaScript", "scripts": { "build": "mkdir -p build && ecmarkup spec.html build/index.html", + "lint": "tsc -p tsconfig.json", "test": "mocha" }, "repository": "legendecas/proposal-async-context", @@ -26,6 +27,7 @@ "@types/mocha": "10.0.1", "@types/node": "18.11.18", "ecmarkup": "^3.1.1", - "mocha": "10.2.0" + "mocha": "10.2.0", + "typescript": "4.9.4" } } diff --git a/src/fork.ts b/src/fork.ts new file mode 100644 index 0000000..0b30195 --- /dev/null +++ b/src/fork.ts @@ -0,0 +1,68 @@ +import type { Mapping } from "./mapping"; +import type { AsyncContext } from "./index"; + +/** + * FrozenRevert holds a frozen Mapping that will be simply restored when the + * revert is run. + * + * This is used when we already know that the mapping is frozen, so that + * reverting will not attempt to mutate the Mapping (and allocate a new + * mapping) as a Revert would. + */ +export class FrozenRevert { + #mapping: Mapping; + + constructor(mapping: Mapping) { + this.#mapping = mapping; + } + + /** + * The Storage container will call restore when it wants to revert its + * current Mapping to the state at the start of the fork. + * + * For FrozenRevert, that's as simple as returning the known-frozen Mapping, + * because we know it can't have been modified. + */ + restore(_current: Mapping): Mapping { + return this.#mapping; + } +} + +/** + * Revert holds the information on how to undo a modification to our Mappings, + * and will attempt to modify the current state when we attempt to restore it + * to its prior state. + * + * This is used when we know that the Mapping is unfrozen at start, because + * it's possible that no one will snapshot this Mapping before we restore. In + * that case, we can simply modify the Mapping without cloning. If someone did + * snapshot it, then modifying will clone the current state and we restore the + * clone to the prior state. + */ +export class Revert { + #key: AsyncContext; + #has: boolean; + #prev: T | undefined; + + constructor(mapping: Mapping, key: AsyncContext) { + this.#key = key; + this.#has = mapping.has(key); + this.#prev = mapping.get(key); + } + + /** + * The Storage container will call restore when it wants to revert its + * current Mapping to the state at the start of the fork. + * + * For Revert, we mutate the known-unfrozen-at-start mapping (which may + * reallocate if anyone has since taken a snapshot) in the hopes that we + * won't need to reallocate. + */ + restore(current: Mapping): Mapping { + if (this.#has) { + return current.set(this.#key, this.#prev); + } else { + return current.delete(this.#key); + } + } +} diff --git a/src/index.ts b/src/index.ts index 0ba6e2a..ec5977b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,45 +1,37 @@ -type AnyFunc = (...args: any) => any; -type Storage = Map, unknown>; +import { Storage } from "./storage"; -let __storage__: Storage = new Map(); +type AnyFunc = (this: T, ...args: any) => any; export class AsyncContext { - static wrap(fn: F): F { - const current = __storage__; + static wrap>(fn: F): F { + const snapshot = Storage.snapshot(); - function wrap(...args: Parameters): ReturnType { - return run(fn, current, this, args); - }; + 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( + run>( value: T, fn: F, ...args: Parameters ): ReturnType { - const next = new Map(__storage__); - next.set(this, value); - return run(fn, next, null, args); + const revert = Storage.set(this, value); + try { + return fn.apply(null, args); + } finally { + Storage.restore(revert); + } } - get(): T { - return __storage__.get(this) as T; - } -} - -function run( - fn: F, - next: Storage, - binding: ThisType, - args: Parameters -): ReturnType { - const previous = __storage__; - try { - __storage__ = next; - return fn.apply(binding, args); - } finally { - __storage__ = previous; + get(): T | undefined { + return Storage.get(this); } } diff --git a/src/mapping.ts b/src/mapping.ts new file mode 100644 index 0000000..bcd2dcf --- /dev/null +++ b/src/mapping.ts @@ -0,0 +1,71 @@ +import type { AsyncContext } from "./index"; + +/** + * Stores all AsyncContext data, and tracks whether any snapshots have been + * taken of the current data. + */ +export class Mapping { + #data: Map, unknown> | null; + + /** + * 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; + + constructor(data: Map, unknown> | null) { + this.#data = data; + this.#frozen = data === null; + } + + has(key: AsyncContext): boolean { + return this.#data?.has(key) || false; + } + + get(key: AsyncContext): 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 { + const mapping = this.#fork(); + mapping.#data!.set(key, value); + return 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 { + const mapping = this.#fork(); + mapping.#data!.delete(key); + return mapping; + } + + /** + * Prevents further modifications to this Mapping. + */ + freeze(): void { + this.#frozen = true; + } + + isFrozen(): boolean { + return this.#frozen; + } + + /** + * We only need to fork if the Mapping is frozen (someone has a snapshot of + * the current data), else we can just modify our data directly. + */ + #fork(): Mapping { + if (this.#frozen) { + return new Mapping(new Map(this.#data)); + } + return this; + } +} diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..d67fcb1 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,70 @@ +import { Mapping } from "./mapping"; +import { FrozenRevert, Revert } from "./fork"; +import type { AsyncContext } from "./index"; + +/** + * Storage is the (internal to the language) storage container of all + * AsyncContext data. + * + * None of the methods here are exposed to users, they're only exposed to the AsyncContext class. + */ +export class Storage { + static #current: Mapping = new Mapping(null); + + /** + * Get retrieves the current value assigned to the AsyncContext. + */ + static get(key: AsyncContext): T | undefined { + return this.#current.get(key); + } + + /** + * Set assigns a new value to the AsyncContext, returning a revert that can + * undo the modification at a later time. + */ + static set(key: AsyncContext, 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() + ? new FrozenRevert(current) + : new Revert(current, key); + this.#current = this.#current.set(key, value); + return undo; + } + + /** + * Restore will, well, restore the global storage state to state at the time + * the revert was created. + */ + static restore(revert: FrozenRevert | Revert): void { + this.#current = revert.restore(this.#current); + } + + /** + * Snapshot freezes the current storage state, and returns a new revert which + * can restore the global storage state to the state at the time of the + * snapshot. + */ + static snapshot(): FrozenRevert { + this.#current.freeze(); + return new FrozenRevert(this.#current); + } + + /** + * Switch swaps the global storage state to the state at the time of a + * snapshot, completely replacing the current state (and making it impossible + * for the current state to be modified until the snapshot is reverted). + */ + static switch(snapshot: FrozenRevert): FrozenRevert { + const previous = this.#current; + this.#current = snapshot.restore(previous); + + // Technically, previous may not be frozen. But we know its state cannot + // change, because the only way to modify it is to restore it to the + // Storage container, and the only way to do that is to have snapshot it. + // So it's either snapshot (and frozen), or it's not and thus cannot be + // modified. + return new FrozenRevert(previous); + } +} diff --git a/tests/async-context.test.ts b/tests/async-context.test.ts index 339630a..f4b5376 100644 --- a/tests/async-context.test.ts +++ b/tests/async-context.test.ts @@ -1,15 +1,36 @@ import { AsyncContext } from "../src/index"; -import { then, nativeThen } from "../src/promise-polyfill"; -import assert from "node:assert/strict"; +import { strict as assert } from "assert"; type Value = { id: number }; -function sleep(ms: number): Promise { - return new Promise((r) => setTimeout(r, ms)); + +const _it = it; +it = (() => { + throw new Error("use `test` function"); +}) as any; + +// Test both from the initial state, and from a run state. +// This is because the initial state might be "frozen", and +// that can cause different code paths. +function test(name: string, fn: () => void) { + _it(name, () => { + fn(); + + // Ensure we're running from a new state, which won't be frozen. + const throwaway = new AsyncContext(); + throwaway.run(null, fn); + + throwaway.run(null, () => { + AsyncContext.wrap(() => {}); + + // Ensure we're running from a new state, which is frozen. + fn(); + }); + }); } describe("sync", () => { describe("run and get", () => { - it("has initial undefined state", () => { + test("has initial undefined state", () => { const ctx = new AsyncContext(); const actual = ctx.get(); @@ -17,7 +38,7 @@ describe("sync", () => { assert.equal(actual, undefined); }); - it("return value", () => { + test("return value", () => { const ctx = new AsyncContext(); const expected = { id: 1 }; @@ -26,224 +47,543 @@ describe("sync", () => { assert.equal(actual, expected); }); - it("get returns current context value", () => { + test("get returns current context value", () => { const ctx = new AsyncContext(); const expected = { id: 1 }; - const actual = ctx.run(expected, () => ctx.get()); - - assert.equal(actual, expected); + ctx.run(expected, () => { + assert.equal(ctx.get(), expected); + }); }); - it("get within nesting contexts", () => { + test("get within nesting contexts", () => { const ctx = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; - const actual = ctx.run(first, () => { - return [ctx.get(), ctx.run(second, () => ctx.get()), ctx.get()]; + ctx.run(first, () => { + assert.equal(ctx.get(), first); + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + assert.equal(ctx.get(), first); }); - - assert.deepStrictEqual(actual, [first, second, first]); + assert.equal(ctx.get(), undefined); }); - it("get within nesting different contexts", () => { + test("get within nesting different contexts", () => { const a = new AsyncContext(); const b = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; - const actual = a.run(first, () => { - return [ - a.get(), - b.get(), - ...b.run(second, () => [a.get(), b.get()]), - a.get(), - b.get(), - ]; - }); - - assert.deepStrictEqual(actual, [ - first, - undefined, - first, - second, - first, - undefined, - ]); + a.run(first, () => { + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + b.run(second, () => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + }); + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + }); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); }); }); describe("wrap", () => { - it("stores initial undefined state", () => { + test("stores initial undefined state", () => { const ctx = new AsyncContext(); const wrapped = AsyncContext.wrap(() => ctx.get()); - const actual = ctx.run({ id: 1 }, () => wrapped()); - - assert.equal(actual, undefined); + ctx.run({ id: 1 }, () => { + assert.equal(wrapped(), undefined); + }); }); - it("stores current state", () => { + test("stores current state", () => { const ctx = new AsyncContext(); const expected = { id: 1 }; - const wrapped = ctx.run(expected, () => { - return AsyncContext.wrap(() => ctx.get()); + const wrap = ctx.run(expected, () => { + const wrap = AsyncContext.wrap(() => ctx.get()); + assert.equal(wrap(), expected); + assert.equal(ctx.get(), expected); + return wrap; }); - const actual = wrapped(); - - assert.equal(actual, expected); + assert.equal(wrap(), expected); + assert.equal(ctx.get(), undefined); }); - it("wrap within nesting contexts", () => { + test("runs within wrap", () => { const ctx = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; - const actual = ctx.run(first, () => { - const wrapped = ctx.run(second, () => { - return AsyncContext.wrap(() => ctx.get()); + const [wrap1, wrap2] = ctx.run(first, () => { + const wrap1 = AsyncContext.wrap(() => { + assert.equal(ctx.get(), first); + + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + + assert.equal(ctx.get(), first); }); - return [ctx.get(), wrapped(), ctx.get()]; + assert.equal(ctx.get(), first); + + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + + const wrap2 = AsyncContext.wrap(() => { + assert.equal(ctx.get(), first); + + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + + assert.equal(ctx.get(), first); + }); + assert.equal(ctx.get(), first); + return [wrap1, wrap2]; }); - assert.deepStrictEqual(actual, [first, second, first]); + wrap1(); + wrap2(); + assert.equal(ctx.get(), undefined); }); - it("wrap out of order", () => { + test("runs within wrap", () => { const ctx = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; - const firstWrap = ctx.run(first, () => { - return AsyncContext.wrap(() => ctx.get()); + const [wrap1, wrap2] = ctx.run(first, () => { + const wrap1 = AsyncContext.wrap(() => { + assert.equal(ctx.get(), first); + + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + + assert.equal(ctx.get(), first); + }); + assert.equal(ctx.get(), first); + + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + + const wrap2 = AsyncContext.wrap(() => { + assert.equal(ctx.get(), first); + + ctx.run(second, () => { + assert.equal(ctx.get(), second); + }); + + assert.equal(ctx.get(), first); + }); + assert.equal(ctx.get(), first); + return [wrap1, wrap2]; }); - const secondWrap = ctx.run(second, () => { - return AsyncContext.wrap(() => ctx.get()); + + wrap1(); + wrap2(); + assert.equal(ctx.get(), undefined); + }); + + test("runs different context within wrap", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const [wrap1, wrap2] = a.run(first, () => { + const wrap1 = AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + + b.run(second, () => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + }); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + }); + + a.run(second, () => {}); + + const wrap2 = AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + + b.run(second, () => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + }); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + }); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + return [wrap1, wrap2]; }); - const actual = [firstWrap(), secondWrap(), firstWrap(), secondWrap()]; - assert.deepStrictEqual(actual, [first, second, first, second]); + wrap1(); + wrap2(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); }); - }); -}); -describe("async via promises", () => { - beforeEach(() => { - Promise.prototype.then = then; - }); - afterEach(() => { - Promise.prototype.then = nativeThen; - }); + test("runs different context within wrap, 2", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; - describe("run and get", () => { - it("get returns current context value", async () => { - const ctx = new AsyncContext(); - const expected = { id: 1 }; + const [wrap1, wrap2] = a.run(first, () => { + const wrap1 = AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); - const actual = await ctx.run(expected, () => { - return Promise.resolve().then(() => ctx.get()); + b.run(second, () => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + }); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + }); + + b.run(second, () => {}); + + const wrap2 = AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + + b.run(second, () => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + }); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + }); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + return [wrap1, wrap2]; }); - assert.equal(actual, expected); + wrap1(); + wrap2(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); }); - it("get within nesting contexts", async () => { + test("wrap within nesting contexts", () => { const ctx = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; - const actual = await ctx.run(first, () => { - return Promise.resolve([]) - .then((temp) => { - temp.push(ctx.get()); - return temp; - }) - .then((temp) => { - return ctx.run(second, () => { - return Promise.resolve().then(() => { - temp.push(ctx.get()); - return temp; - }); + const [firstWrap, secondWrap] = ctx.run(first, () => { + const firstWrap = AsyncContext.wrap(() => { + assert.equal(ctx.get(), first); + }); + firstWrap(); + + const secondWrap = ctx.run(second, () => { + const secondWrap = AsyncContext.wrap(() => { + firstWrap(); + assert.equal(ctx.get(), second); + }); + firstWrap(); + secondWrap(); + assert.equal(ctx.get(), second); + + return secondWrap; + }); + + firstWrap(); + secondWrap(); + assert.equal(ctx.get(), first); + + return [firstWrap, secondWrap]; + }); + + firstWrap(); + secondWrap(); + assert.equal(ctx.get(), undefined); + }); + + test("wrap within nesting different contexts", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const [firstWrap, secondWrap] = a.run(first, () => { + const firstWrap = AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + }); + firstWrap(); + + const secondWrap = b.run(second, () => { + const secondWrap = AsyncContext.wrap(() => { + firstWrap(); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + }); + + firstWrap(); + secondWrap(); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + + return secondWrap; + }); + + firstWrap(); + secondWrap(); + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + + return [firstWrap, secondWrap]; + }); + + firstWrap(); + secondWrap(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + }); + + test("wrap within nesting different contexts, 2", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const c = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + const third = { id: 3 }; + + const wrap = a.run(first, () => { + const wrap = b.run(second, () => { + const wrap = c.run(third, () => { + return AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), third); + }); + }); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), undefined); + return wrap; + }); + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + + return wrap; + }); + + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + wrap(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + }); + + test("wrap within nesting different contexts, 3", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const c = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + const third = { id: 3 }; + + const wrap = a.run(first, () => { + const wrap = b.run(second, () => { + AsyncContext.wrap(() => {}); + + const wrap = c.run(third, () => { + return AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), third); }); - }) - .then((temp) => { - temp.push(ctx.get()); - return temp; }); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), undefined); + return wrap; + }); + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + + return wrap; }); - assert.deepStrictEqual(actual, [first, second, first]); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + wrap(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); }); - it("get within nesting different contexts", async () => { + test("wrap within nesting different contexts, 4", () => { const a = new AsyncContext(); const b = new AsyncContext(); + const c = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; + const third = { id: 3 }; + + const wrap = a.run(first, () => { + AsyncContext.wrap(() => {}); + + const wrap = b.run(second, () => { + const wrap = c.run(third, () => { + return AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), third); + }); + }); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), undefined); + return wrap; + }); + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + + return wrap; + }); + + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + wrap(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + }); + + test("wrap within nesting different contexts, 5", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const c = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + const third = { id: 3 }; + + const wrap = a.run(first, () => { + const wrap = b.run(second, () => { + const wrap = c.run(third, () => { + return AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), third); + }); + }); + + AsyncContext.wrap(() => {}); - const actual = await a.run(first, () => { - return Promise.resolve([]) - .then((temp) => { - temp.push(a.get(), b.get()); - return temp; - }) - .then((temp) => { - return b.run(second, () => { - return Promise.resolve().then(() => { - temp.push(a.get(), b.get()); - return temp; - }); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), undefined); + return wrap; + }); + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + + return wrap; + }); + + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + wrap(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + }); + + test("wrap within nesting different contexts, 6", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const c = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + const third = { id: 3 }; + + const wrap = a.run(first, () => { + const wrap = b.run(second, () => { + const wrap = c.run(third, () => { + return AsyncContext.wrap(() => { + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), third); }); - }) - .then((temp) => { - temp.push(a.get(), b.get()); - return temp; }); + assert.equal(a.get(), first); + assert.equal(b.get(), second); + assert.equal(c.get(), undefined); + return wrap; + }); + + AsyncContext.wrap(() => {}); + + assert.equal(a.get(), first); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + + return wrap; }); - assert.deepStrictEqual(actual, [ - first, - undefined, - first, - second, - first, - undefined, - ]); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); + wrap(); + assert.equal(a.get(), undefined); + assert.equal(b.get(), undefined); + assert.equal(c.get(), undefined); }); - it("get out of order", async () => { + test("wrap out of order", () => { const ctx = new AsyncContext(); const first = { id: 1 }; const second = { id: 2 }; - const firstRun = ctx.run(first, () => { - return [ - sleep(10).then(() => ctx.get()), - sleep(20).then(() => ctx.get()), - sleep(30).then(() => ctx.get()), - ]; - }); - const secondRun = ctx.run(second, () => { - return [ - sleep(25).then(() => ctx.get()), - sleep(15).then(() => ctx.get()), - sleep(5).then(() => ctx.get()), - ]; - }); - - const actual = await Promise.all(firstRun.concat(secondRun)); - - assert.deepStrictEqual(actual, [ - first, - first, - first, - second, - second, - second, - ]); + const firstWrap = ctx.run(first, () => { + return AsyncContext.wrap(() => { + assert.equal(ctx.get(), first); + }); + }); + const secondWrap = ctx.run(second, () => { + return AsyncContext.wrap(() => { + assert.equal(ctx.get(), second); + }); + }); + + firstWrap(); + secondWrap(); + firstWrap(); + secondWrap(); }); }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c5b879a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "allowSyntheticDefaultImports": true, + "moduleResolution": "nodenext", + "noEmit": true + } +}