als.spec.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. /**
  2. * Sync-stack AsyncLocalStorage semantics plus the causality faces the module
  3. * loader's `await` rewriting consumes: run boundaries, ambient snapshots, and
  4. * the context root the tunnel dispatches at.
  5. */
  6. import { describe, expect, it } from 'vitest'
  7. import {
  8. AsyncLocalStorage, alsCausality, captureAsyncContext, runAtAsyncContextRoot, runWithAsyncContext,
  9. } from '../../src/node/builtin_modules/implemented/async_hooks.ts'
  10. describe('AsyncLocalStorage', () => {
  11. it('publishes the store inside run and clears it outside', () => {
  12. const als = new AsyncLocalStorage<string>()
  13. expect(als.getStore()).toBeUndefined()
  14. const returned = als.run('inner', () => {
  15. expect(als.getStore()).toBe('inner')
  16. return 42
  17. })
  18. expect(returned).toBe(42)
  19. expect(als.getStore()).toBeUndefined()
  20. })
  21. it('shadows and restores across nested boundaries', () => {
  22. const als = new AsyncLocalStorage<string>()
  23. als.run('outer', () => {
  24. expect(als.getStore()).toBe('outer')
  25. als.run('inner', () => { expect(als.getStore()).toBe('inner') })
  26. expect(als.getStore()).toBe('outer')
  27. })
  28. })
  29. it('keeps the store visible until a returned promise settles', async () => {
  30. const als = new AsyncLocalStorage<string>()
  31. let during: string | undefined
  32. await als.run('async', async () => {
  33. await Promise.resolve()
  34. during = als.getStore()
  35. })
  36. expect(during).toBe('async')
  37. })
  38. })
  39. describe('causality faces', () => {
  40. it('captures a context and republishes it inside runWithAsyncContext', () => {
  41. const als = new AsyncLocalStorage<string>()
  42. let snapshot: ReturnType<typeof captureAsyncContext>
  43. als.run('captured', () => { snapshot = captureAsyncContext() })
  44. expect(als.getStore()).toBeUndefined()
  45. runWithAsyncContext(snapshot, () => {
  46. expect(als.getStore()).toBe('captured')
  47. })
  48. expect(als.getStore()).toBeUndefined()
  49. })
  50. it('restores every live instance through the alsCausality pair', () => {
  51. const first = new AsyncLocalStorage<string>()
  52. const second = new AsyncLocalStorage<number>()
  53. let paused: ReturnType<typeof alsCausality.snapshot> | undefined
  54. first.run('a', () => {
  55. second.run(7, () => { paused = alsCausality.snapshot() })
  56. })
  57. expect(first.getStore()).toBeUndefined()
  58. expect(second.getStore()).toBeUndefined()
  59. // The rewriting calls restore at a resume point with no place for a
  60. // disposer; the published context answers reads after the await.
  61. alsCausality.restore(paused!)
  62. expect(first.getStore()).toBe('a')
  63. expect(second.getStore()).toBe(7)
  64. // A fresh boundary still wins over the resumed ambient context.
  65. first.run('b', () => { expect(first.getStore()).toBe('b') })
  66. })
  67. it('dispatches at the context root without inheriting the caller boundary', () => {
  68. const als = new AsyncLocalStorage<string>()
  69. als.run('caller', () => {
  70. runAtAsyncContextRoot(() => {
  71. expect(als.getStore()).toBeUndefined()
  72. })
  73. expect(als.getStore()).toBe('caller')
  74. })
  75. })
  76. })