From 4c51377e6ffad7374aae30e0c9663db8b9dbc5bd Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 21 May 2024 15:43:26 +0200 Subject: [PATCH] Explicit create() and run() (#5) * Explicit create() and run() Signed-off-by: Matteo Collina * add node 22 Signed-off-by: Matteo Collina --------- Signed-off-by: Matteo Collina --- .github/workflows/ci.yml | 2 +- README.md | 41 +++++++------ asyncforge.d.ts | 8 ++- asyncforge.js | 46 +++++++++------ asyncforge.test-d.ts | 14 ++--- example.mjs | 41 +++++++------ test/memo.test.js | 124 ++++++++++++++++++++++----------------- test/setall.test.js | 39 ------------ 8 files changed, 159 insertions(+), 156 deletions(-) delete mode 100644 test/setall.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec0c55e..2c80f7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index bdb3e11..bc82aa0 100644 --- a/README.md +++ b/README.md @@ -14,36 +14,43 @@ npm i asyncforge ## Usage ```js -import { memo } from 'asyncforge' +import { create, memo } from 'asyncforge.js' const a = memo() const b = memo() -a.set(42) -b.set(123) +const store = create() -// simulate an event loop turn -setImmediate(() => { - console.log('-- first event loop turn --') - console.log('a', a()) - console.log('b', b()) +store.run(() => { + a.set(42) + b.set(123) - b.set(456) + // simulate an event loop turn setImmediate(() => { - console.log('-- third event loop turn --') + console.log('-- first event loop turn --') console.log('a', a()) console.log('b', b()) }) }) -a.set(43) -b.set(321) +create(() => { + a.set(43) + b.set(321) -// simulate an event loop turn -setImmediate(() => { - console.log('-- second event loop turn --') - console.log('a', a()) - console.log('b', b()) + // simulate an event loop turn + setImmediate(() => { + console.log('-- second event loop turn --') + console.log('a', a()) + console.log('b', b()) + + store.run(() => { + setImmediate(() => { + console.log('-- third event loop turn --') + console.log('a', a()) + console.log('b', b()) + }) + }) + }) }) ``` diff --git a/asyncforge.d.ts b/asyncforge.d.ts index 1cef37c..97885ba 100644 --- a/asyncforge.d.ts +++ b/asyncforge.d.ts @@ -1,5 +1,10 @@ declare namespace asyncforge { - export function start(): void; + interface Store { + run(fn: () => T) : T; + } + + export function create () : Store; + export function memo( name?: string ): { @@ -7,7 +12,6 @@ declare namespace asyncforge { key: symbol; set: (value: T) => void; }; - export function setAll(memos: Record): void; } export = asyncforge diff --git a/asyncforge.js b/asyncforge.js index 5f39368..2bfe3e4 100644 --- a/asyncforge.js +++ b/asyncforge.js @@ -4,9 +4,24 @@ const { AsyncLocalStorage } = require('node:async_hooks') const asyncLocalStorage = new AsyncLocalStorage() -function start () { - const store = Object.create(null) - asyncLocalStorage.enterWith(store) +class Store { + #internal + + constructor (internal) { + this.#internal = internal + } + + run (fn) { + return asyncLocalStorage.run(this.#internal, fn) + } +} + +function create (fn) { + const store = new Store(Object.create(null)) + if (fn) { + store.run(fn) + } + return store } let memoCounter = 0 @@ -18,16 +33,20 @@ function memo (name) { function get () { const store = asyncLocalStorage.getStore() if (!store) { - throw new Error(`asyncforge store is not initialized for ${name}`) + throw new Error('asyncforge store has not been created') } return store[sym] } function set (value) { - let store = asyncLocalStorage.getStore() - store = Object.create(store || null) + const store = asyncLocalStorage.getStore() + if (!store) { + throw new Error('asyncforge store has not been created') + } + if (Object.hasOwnProperty.call(store, sym)) { + throw new Error(`asyncforge store already initialized for ${name}`) + } store[sym] = value - asyncLocalStorage.enterWith(store) } get.set = set @@ -36,16 +55,5 @@ function memo (name) { return get } -function setAll (memos) { - let store = asyncLocalStorage.getStore() - store = Object.create(store || null) - const keys = Object.getOwnPropertySymbols(memos) - for (const key of keys) { - store[key] = memos[key] - } - asyncLocalStorage.enterWith(store) -} - +module.exports.create = create module.exports.memo = memo -module.exports.start = start -module.exports.setAll = setAll diff --git a/asyncforge.test-d.ts b/asyncforge.test-d.ts index 6fcef1b..15791a3 100644 --- a/asyncforge.test-d.ts +++ b/asyncforge.test-d.ts @@ -1,7 +1,12 @@ import { expectError, expectType } from "tsd"; -import { start, memo, setAll } from "."; +import { create, memo } from "."; +import type { Store } from "."; -expectType<() => void>(start); +expectType<() => Store>(create); + +const store = create(); + +expectType(store.run(() => 42)); // memo const memoNum = memo(); @@ -9,8 +14,3 @@ expectType(memoNum.key); expectType(memoNum.set(123)); expectType(memoNum()); expectError(memoNum.set("wrong")); - -// setAll -const test = memo(); -expectType(setAll({ [test.key]: 42 })); -expectError(setAll({ wrong: 42 })); diff --git a/example.mjs b/example.mjs index 9b5a73b..07dc9fe 100644 --- a/example.mjs +++ b/example.mjs @@ -1,31 +1,38 @@ -import { memo } from './asyncforge.js' +import { create, memo } from './asyncforge.js' const a = memo() const b = memo() -a.set(42) -b.set(123) +const store = create() -// simulate an event loop turn -setImmediate(() => { - console.log('-- first event loop turn --') - console.log('a', a()) - console.log('b', b()) +store.run(() => { + a.set(42) + b.set(123) - b.set(456) + // simulate an event loop turn setImmediate(() => { - console.log('-- third event loop turn --') + console.log('-- first event loop turn --') console.log('a', a()) console.log('b', b()) }) }) -a.set(43) -b.set(321) +create(() => { + a.set(43) + b.set(321) -// simulate an event loop turn -setImmediate(() => { - console.log('-- second event loop turn --') - console.log('a', a()) - console.log('b', b()) + // simulate an event loop turn + setImmediate(() => { + console.log('-- second event loop turn --') + console.log('a', a()) + console.log('b', b()) + + store.run(() => { + setImmediate(() => { + console.log('-- third event loop turn --') + console.log('a', a()) + console.log('b', b()) + }) + }) + }) }) diff --git a/test/memo.test.js b/test/memo.test.js index b9ca27b..3a77b68 100644 --- a/test/memo.test.js +++ b/test/memo.test.js @@ -1,101 +1,117 @@ 'use strict' const { test } = require('node:test') -const { start, memo } = require('../') +const { create, memo } = require('../') const tspl = require('@matteo.collina/tspl') +const assert = require('node:assert') test('memo', async (t) => { - const p = tspl(t, { plan: 7 }) + const p = tspl(t, { plan: 8 }) const a = memo() - p.throws(a, /asyncforge store is not initialized for memo0/) - - start() - - p.deepStrictEqual(a(), undefined) - a.set({ value: 'bar' }) - p.deepStrictEqual(a(), { value: 'bar' }) + p.throws(a, /asyncforge store has not been created/) + p.throws(() => a.set('foo'), /asyncforge store has not been created/) - setImmediate(() => { + create().run(() => { + p.deepStrictEqual(a(), undefined) + a.set({ value: 'bar' }) p.deepStrictEqual(a(), { value: 'bar' }) - }) - queueMicrotask(() => { - p.deepStrictEqual(a(), { value: 'bar' }) - }) + setImmediate(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + }) - start() + queueMicrotask(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + }) + }) - p.deepStrictEqual(a(), undefined) - a.set({ value: 'baz' }) + create(() => { + p.deepStrictEqual(a(), undefined) + a.set({ value: 'baz' }) - p.deepEqual(a(), { value: 'baz' }) + p.deepEqual(a(), { value: 'baz' }) - setImmediate(() => { - p.deepStrictEqual(a(), { value: 'baz' }) - }) + setImmediate(() => { + p.deepStrictEqual(a(), { value: 'baz' }) + }) - queueMicrotask(() => { - p.deepStrictEqual(a(), { value: 'baz' }) + queueMicrotask(() => { + p.deepStrictEqual(a(), { value: 'baz' }) + }) }) await p.completed }) -test('nested', async (t) => { - const p = tspl(t, { plan: 5 }) +test('overriding the store should throw', (t) => { const a = memo() + create().run(() => { + a.set({ value: 'bar' }) + assert.throws(() => { + a.set({ value: 'baz' }) + }, /asyncforge store already initialized for memo\d+/) + }) +}) - start() - - p.deepStrictEqual(a(), undefined) - a.set({ value: 'bar' }) - p.deepStrictEqual(a(), { value: 'bar' }) +test('restarted', async (t) => { + const p = tspl(t, { plan: 5 }) + const a = memo() - setImmediate(() => { + create().run(() => { + p.deepStrictEqual(a(), undefined) + a.set({ value: 'bar' }) p.deepStrictEqual(a(), { value: 'bar' }) - a.set({ value: 'baz' }) - setImmediate(() => { - p.deepStrictEqual(a(), { value: 'baz' }) + p.deepStrictEqual(a(), { value: 'bar' }) }) }) - setImmediate(() => { - p.deepStrictEqual(a(), { value: 'bar' }) + create().run(() => { + p.deepStrictEqual(a(), undefined) + + setImmediate(() => { + p.deepStrictEqual(a(), undefined) + }) }) await p.completed }) -test('memo without start', async (t) => { - const p = tspl(t, { plan: 6 }) +test('run multiple times', async (t) => { + const p = tspl(t, { plan: 7 }) const a = memo() - p.throws(a, /asyncforge store is not initialized for memo\d+/) - a.set({ value: 'bar' }) - p.deepStrictEqual(a(), { value: 'bar' }) + p.throws(a, /asyncforge store has not been created/) - setImmediate(() => { - p.deepStrictEqual(a(), { value: 'bar' }) - }) + const store = create() - queueMicrotask(() => { + store.run(() => { + p.deepStrictEqual(a(), undefined) + a.set({ value: 'bar' }) p.deepStrictEqual(a(), { value: 'bar' }) - }) - - p.deepStrictEqual(a(), { value: 'bar' }) - a.set({ value: 'baz' }) - p.deepEqual(a(), { value: 'baz' }) + setImmediate(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + }) - setImmediate(() => { - p.deepStrictEqual(a(), { value: 'baz' }) + queueMicrotask(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + }) }) - queueMicrotask(() => { - p.deepStrictEqual(a(), { value: 'baz' }) + store.run(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + p.throws(() => a.set({ value: 'baz' }), /asyncforge store already initialized for memo\d+/) + + setImmediate(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + }) + + queueMicrotask(() => { + p.deepStrictEqual(a(), { value: 'bar' }) + }) }) await p.completed diff --git a/test/setall.test.js b/test/setall.test.js deleted file mode 100644 index a2bd857..0000000 --- a/test/setall.test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const { start, memo, setAll } = require('../') -const assert = require('node:assert/strict') - -test('setAll no start', async (t) => { - const a = memo() - const b = memo() - const c = memo() - - setAll({ - [a.key]: 1, - [b.key]: 2, - [c.key]: 3 - }) - - assert.equal(a(), 1) - assert.equal(b(), 2) - assert.equal(c(), 3) -}) - -test('setAll memos', async (t) => { - const a = memo() - const b = memo() - const c = memo() - - start() - - setAll({ - [a.key]: 1, - [b.key]: 2, - [c.key]: 3 - }) - - assert.equal(a(), 1) - assert.equal(b(), 2) - assert.equal(c(), 3) -})