@declaro/core 2.0.0-y.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LICENSE → LICENSE.md} +1 -1
- package/README.md +203 -0
- package/dist/browser/index.js +28 -0
- package/dist/browser/index.js.map +133 -0
- package/dist/browser/scope/index.js +3 -0
- package/dist/browser/scope/index.js.map +9 -0
- package/dist/bun/index.js +19011 -0
- package/dist/bun/index.js.map +132 -0
- package/dist/bun/scope/index.js +4 -0
- package/dist/bun/scope/index.js.map +9 -0
- package/dist/node/index.cjs +19039 -0
- package/dist/node/index.cjs.map +132 -0
- package/dist/node/index.js +19010 -0
- package/dist/node/index.js.map +132 -0
- package/dist/node/scope/index.cjs +69 -0
- package/dist/node/scope/index.cjs.map +9 -0
- package/dist/node/scope/index.js +3 -0
- package/dist/node/scope/index.js.map +9 -0
- package/dist/ts/app/app-context.d.ts +9 -0
- package/dist/ts/app/app-context.d.ts.map +1 -0
- package/dist/ts/app/app-lifecycle.d.ts +6 -0
- package/dist/ts/app/app-lifecycle.d.ts.map +1 -0
- package/dist/ts/app/app.d.ts +24 -0
- package/dist/ts/app/app.d.ts.map +1 -0
- package/dist/{app → ts/app}/index.d.ts +1 -0
- package/dist/ts/app/index.d.ts.map +1 -0
- package/dist/ts/application/create-request-context.d.ts +4 -0
- package/dist/ts/application/create-request-context.d.ts.map +1 -0
- package/dist/ts/application/create-request-context.test.d.ts +2 -0
- package/dist/ts/application/create-request-context.test.d.ts.map +1 -0
- package/dist/ts/application/use-declaro.d.ts +3 -0
- package/dist/ts/application/use-declaro.d.ts.map +1 -0
- package/dist/{auth → ts/auth}/permission-validator.d.ts +1 -0
- package/dist/ts/auth/permission-validator.d.ts.map +1 -0
- package/dist/ts/auth/permission-validator.test.d.ts +2 -0
- package/dist/ts/auth/permission-validator.test.d.ts.map +1 -0
- package/dist/ts/context/async-context.d.ts +54 -0
- package/dist/ts/context/async-context.d.ts.map +1 -0
- package/dist/ts/context/async-context.test.d.ts +2 -0
- package/dist/ts/context/async-context.test.d.ts.map +1 -0
- package/dist/{context → ts/context}/context-consumer.d.ts +4 -0
- package/dist/ts/context/context-consumer.d.ts.map +1 -0
- package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
- package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
- package/dist/ts/context/context.d.ts +452 -0
- package/dist/ts/context/context.d.ts.map +1 -0
- package/dist/ts/context/context.test.d.ts +2 -0
- package/dist/ts/context/context.test.d.ts.map +1 -0
- package/dist/ts/context/legacy-context.test.d.ts +2 -0
- package/dist/ts/context/legacy-context.test.d.ts.map +1 -0
- package/dist/{context → ts/context}/validators.d.ts +2 -1
- package/dist/ts/context/validators.d.ts.map +1 -0
- package/dist/ts/dataflow/index.d.ts +2 -0
- package/dist/ts/dataflow/index.d.ts.map +1 -0
- package/dist/ts/dataflow/objects.d.ts +7 -0
- package/dist/ts/dataflow/objects.d.ts.map +1 -0
- package/dist/ts/dataflow/objects.test.d.ts +2 -0
- package/dist/ts/dataflow/objects.test.d.ts.map +1 -0
- package/dist/{errors → ts/errors}/errors.d.ts +16 -3
- package/dist/ts/errors/errors.d.ts.map +1 -0
- package/dist/ts/events/event-manager.d.ts +19 -0
- package/dist/ts/events/event-manager.d.ts.map +1 -0
- package/dist/ts/events/event-manager.spec.d.ts +2 -0
- package/dist/ts/events/event-manager.spec.d.ts.map +1 -0
- package/dist/ts/events/index.d.ts +2 -0
- package/dist/ts/events/index.d.ts.map +1 -0
- package/dist/ts/http/headers.d.ts +21 -0
- package/dist/ts/http/headers.d.ts.map +1 -0
- package/dist/ts/http/headers.spec.d.ts +2 -0
- package/dist/ts/http/headers.spec.d.ts.map +1 -0
- package/dist/ts/http/request-context.d.ts +17 -0
- package/dist/ts/http/request-context.d.ts.map +1 -0
- package/dist/ts/http/request-context.spec.d.ts +2 -0
- package/dist/ts/http/request-context.spec.d.ts.map +1 -0
- package/dist/ts/http/request.d.ts +31 -0
- package/dist/ts/http/request.d.ts.map +1 -0
- package/dist/ts/http/request.spec.d.ts +2 -0
- package/dist/ts/http/request.spec.d.ts.map +1 -0
- package/dist/{http → ts/http}/url.d.ts +5 -4
- package/dist/ts/http/url.d.ts.map +1 -0
- package/dist/ts/http/url.spec.d.ts +2 -0
- package/dist/ts/http/url.spec.d.ts.map +1 -0
- package/dist/ts/index.d.ts +47 -0
- package/dist/ts/index.d.ts.map +1 -0
- package/dist/{pipelines → ts/pipelines}/index.d.ts +1 -0
- package/dist/ts/pipelines/index.d.ts.map +1 -0
- package/dist/{pipelines → ts/pipelines}/pipeline-action.d.ts +1 -0
- package/dist/ts/pipelines/pipeline-action.d.ts.map +1 -0
- package/dist/ts/pipelines/pipeline-action.test.d.ts +2 -0
- package/dist/ts/pipelines/pipeline-action.test.d.ts.map +1 -0
- package/dist/{pipelines → ts/pipelines}/pipeline.d.ts +3 -2
- package/dist/ts/pipelines/pipeline.d.ts.map +1 -0
- package/dist/ts/pipelines/pipeline.test.d.ts +2 -0
- package/dist/ts/pipelines/pipeline.test.d.ts.map +1 -0
- package/dist/ts/schema/json-schema.d.ts +12 -0
- package/dist/ts/schema/json-schema.d.ts.map +1 -0
- package/dist/ts/schema/labels.d.ts +14 -0
- package/dist/ts/schema/labels.d.ts.map +1 -0
- package/dist/ts/schema/model-schema.d.ts +75 -0
- package/dist/ts/schema/model-schema.d.ts.map +1 -0
- package/dist/ts/schema/model-schema.test.d.ts +2 -0
- package/dist/ts/schema/model-schema.test.d.ts.map +1 -0
- package/dist/ts/schema/model.d.ts +35 -0
- package/dist/ts/schema/model.d.ts.map +1 -0
- package/dist/ts/schema/schema-mixin.d.ts +24 -0
- package/dist/ts/schema/schema-mixin.d.ts.map +1 -0
- package/dist/ts/schema/test/mock-model.d.ts +8 -0
- package/dist/ts/schema/test/mock-model.d.ts.map +1 -0
- package/dist/ts/scope/index.d.ts +34 -0
- package/dist/ts/scope/index.d.ts.map +1 -0
- package/dist/ts/shared/utils/action-descriptor.d.ts +28 -0
- package/dist/ts/shared/utils/action-descriptor.d.ts.map +1 -0
- package/dist/ts/shared/utils/action-descriptor.test.d.ts +2 -0
- package/dist/ts/shared/utils/action-descriptor.test.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
- package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
- package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
- package/dist/ts/shims/async-local-storage.d.ts +36 -0
- package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
- package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
- package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
- package/dist/{timing.d.ts → ts/timing.d.ts} +1 -0
- package/dist/ts/timing.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/arrays.d.ts +1 -0
- package/dist/ts/typescript/arrays.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/baseModel.d.ts +1 -0
- package/dist/ts/typescript/baseModel.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/classes.d.ts +1 -0
- package/dist/ts/typescript/classes.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/constant-manipulation/snake-case.d.ts +1 -0
- package/dist/ts/typescript/constant-manipulation/snake-case.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/errors.d.ts +1 -0
- package/dist/ts/typescript/errors.d.ts.map +1 -0
- package/dist/ts/typescript/fetch.d.ts +3 -0
- package/dist/ts/typescript/fetch.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/generics.d.ts +1 -0
- package/dist/ts/typescript/generics.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/index.d.ts +1 -0
- package/dist/ts/typescript/index.d.ts.map +1 -0
- package/dist/ts/typescript/objects.d.ts +26 -0
- package/dist/ts/typescript/objects.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/promises.d.ts +1 -0
- package/dist/ts/typescript/promises.d.ts.map +1 -0
- package/dist/{validation → ts/validation}/index.d.ts +1 -0
- package/dist/ts/validation/index.d.ts.map +1 -0
- package/dist/{validation → ts/validation}/validation.d.ts +1 -0
- package/dist/ts/validation/validation.d.ts.map +1 -0
- package/dist/{validation → ts/validation}/validator.d.ts +1 -0
- package/dist/ts/validation/validator.d.ts.map +1 -0
- package/dist/ts/validation/validator.test.d.ts +2 -0
- package/dist/ts/validation/validator.test.d.ts.map +1 -0
- package/package.json +46 -13
- package/src/app/app-context.ts +4 -5
- package/src/app/app-lifecycle.ts +4 -3
- package/src/app/app.ts +7 -5
- package/src/application/create-request-context.test.ts +345 -0
- package/src/application/create-request-context.ts +19 -0
- package/src/application/use-declaro.ts +27 -0
- package/src/auth/permission-validator.test.ts +238 -2
- package/src/auth/permission-validator.ts +3 -3
- package/src/context/async-context.test.ts +348 -0
- package/src/context/async-context.ts +129 -0
- package/src/context/context-consumer.ts +4 -4
- package/src/context/context.circular-deps.test.ts +1047 -0
- package/src/context/context.test.ts +420 -3
- package/src/context/context.ts +590 -87
- package/src/context/legacy-context.test.ts +9 -9
- package/src/dataflow/objects.test.ts +7 -7
- package/src/dataflow/objects.ts +10 -9
- package/src/errors/errors.ts +19 -3
- package/src/events/event-manager.spec.ts +129 -0
- package/src/events/event-manager.ts +25 -14
- package/src/http/headers.ts +17 -2
- package/src/http/request-context.ts +24 -15
- package/src/http/request.ts +27 -6
- package/src/http/url.ts +3 -3
- package/src/index.ts +34 -3
- package/src/pipelines/pipeline.test.ts +11 -9
- package/src/schema/json-schema.ts +16 -0
- package/src/schema/labels.ts +23 -23
- package/src/schema/model-schema.test.ts +282 -0
- package/src/schema/model-schema.ts +197 -0
- package/src/schema/model.ts +143 -0
- package/src/schema/schema-mixin.ts +51 -0
- package/src/schema/test/mock-model.ts +19 -0
- package/src/scope/index.ts +33 -0
- package/src/shared/utils/action-descriptor.test.ts +182 -0
- package/src/shared/utils/action-descriptor.ts +102 -0
- package/src/shared/utils/schema-utils.test.ts +33 -0
- package/src/shared/utils/schema-utils.ts +17 -0
- package/src/shims/async-local-storage.test.ts +258 -0
- package/src/shims/async-local-storage.ts +82 -0
- package/src/typescript/objects.ts +32 -1
- package/src/validation/validator.test.ts +12 -20
- package/dist/app/app-context.d.ts +0 -8
- package/dist/app/app-lifecycle.d.ts +0 -4
- package/dist/app/app.d.ts +0 -22
- package/dist/auth/permission-validator.test.d.ts +0 -1
- package/dist/context/context.d.ts +0 -161
- package/dist/context/context.test.d.ts +0 -1
- package/dist/context/legacy-context.test.d.ts +0 -1
- package/dist/dataflow/index.d.ts +0 -1
- package/dist/dataflow/objects.d.ts +0 -5
- package/dist/dataflow/objects.test.d.ts +0 -1
- package/dist/events/event-manager.d.ts +0 -16
- package/dist/events/event-manager.spec.d.ts +0 -1
- package/dist/events/index.d.ts +0 -1
- package/dist/helpers/index.d.ts +0 -1
- package/dist/helpers/ucfirst.d.ts +0 -1
- package/dist/http/headers.d.ts +0 -4
- package/dist/http/headers.spec.d.ts +0 -1
- package/dist/http/request-context.d.ts +0 -12
- package/dist/http/request-context.spec.d.ts +0 -1
- package/dist/http/request.d.ts +0 -8
- package/dist/http/request.spec.d.ts +0 -1
- package/dist/http/url.spec.d.ts +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/pipelines/pipeline-action.test.d.ts +0 -1
- package/dist/pipelines/pipeline.test.d.ts +0 -1
- package/dist/pkg.cjs +0 -30
- package/dist/pkg.mjs +0 -56612
- package/dist/schema/application.d.ts +0 -83
- package/dist/schema/application.test.d.ts +0 -1
- package/dist/schema/define-model.d.ts +0 -10
- package/dist/schema/define-model.test.d.ts +0 -1
- package/dist/schema/formats.d.ts +0 -10
- package/dist/schema/index.d.ts +0 -10
- package/dist/schema/labels.d.ts +0 -13
- package/dist/schema/labels.test.d.ts +0 -1
- package/dist/schema/module.d.ts +0 -7
- package/dist/schema/module.test.d.ts +0 -1
- package/dist/schema/properties.d.ts +0 -19
- package/dist/schema/response.d.ts +0 -31
- package/dist/schema/response.test.d.ts +0 -1
- package/dist/schema/supported-types.d.ts +0 -12
- package/dist/schema/supported-types.test.d.ts +0 -1
- package/dist/schema/transform-model.d.ts +0 -4
- package/dist/schema/transform-model.test.d.ts +0 -1
- package/dist/schema/types.d.ts +0 -95
- package/dist/schema/types.test.d.ts +0 -1
- package/dist/typescript/fetch.d.ts +0 -2
- package/dist/typescript/objects.d.ts +0 -12
- package/dist/validation/validator.test.d.ts +0 -1
- package/src/helpers/index.ts +0 -1
- package/src/helpers/ucfirst.ts +0 -3
- package/src/schema/application.test.ts +0 -286
- package/src/schema/application.ts +0 -150
- package/src/schema/define-model.test.ts +0 -81
- package/src/schema/define-model.ts +0 -50
- package/src/schema/formats.ts +0 -23
- package/src/schema/index.ts +0 -10
- package/src/schema/labels.test.ts +0 -60
- package/src/schema/module.test.ts +0 -39
- package/src/schema/module.ts +0 -6
- package/src/schema/properties.ts +0 -40
- package/src/schema/response.test.ts +0 -101
- package/src/schema/response.ts +0 -93
- package/src/schema/supported-types.test.ts +0 -20
- package/src/schema/supported-types.ts +0 -15
- package/src/schema/transform-model.test.ts +0 -31
- package/src/schema/transform-model.ts +0 -24
- package/src/schema/types.test.ts +0 -28
- package/src/schema/types.ts +0 -163
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -24
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { AsyncLocalStorage as NativeALS } from 'node:async_hooks'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { AsyncLocalStorage as ShimALS } from '../shims/async-local-storage'
|
|
4
|
+
import {
|
|
5
|
+
type AsyncContextStorage,
|
|
6
|
+
createContextAPI,
|
|
7
|
+
useContext,
|
|
8
|
+
withContext,
|
|
9
|
+
} from './async-context'
|
|
10
|
+
import { Context } from './context'
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Shared test suite — sync behavior that works for every storage implementation.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function sharedContextTests(storage: AsyncContextStorage<Context> | undefined) {
|
|
16
|
+
const { withContext, useContext } = createContextAPI<Context>(storage)
|
|
17
|
+
|
|
18
|
+
it('returns the fn result synchronously', () => {
|
|
19
|
+
const context = new Context()
|
|
20
|
+
const result = withContext(context, () => 42)
|
|
21
|
+
expect(result).toBe(42)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns the fn result asynchronously', async () => {
|
|
25
|
+
const context = new Context()
|
|
26
|
+
const result = await withContext(context, async () => 'hello')
|
|
27
|
+
expect(result).toBe('hello')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('useContext() returns the active context inside withContext', () => {
|
|
31
|
+
const context = new Context()
|
|
32
|
+
withContext(context, () => {
|
|
33
|
+
expect(useContext()).toBe(context)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('useContext() returns null outside of withContext', () => {
|
|
38
|
+
expect(useContext()).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('useContext({ strict: true }) throws outside of withContext', () => {
|
|
42
|
+
expect(() => useContext({ strict: true })).toThrow(
|
|
43
|
+
'useContext() was called outside of an active context'
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('useContext({ strict: true }) returns context inside withContext', () => {
|
|
48
|
+
const context = new Context()
|
|
49
|
+
withContext(context, () => {
|
|
50
|
+
expect(useContext({ strict: true })).toBe(context)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('nested withContext calls use the innermost context', () => {
|
|
55
|
+
const outer = new Context()
|
|
56
|
+
const inner = new Context()
|
|
57
|
+
|
|
58
|
+
withContext(outer, () => {
|
|
59
|
+
expect(useContext()).toBe(outer)
|
|
60
|
+
|
|
61
|
+
withContext(inner, () => {
|
|
62
|
+
expect(useContext()).toBe(inner)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(useContext()).toBe(outer)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('context is not visible after withContext completes', () => {
|
|
70
|
+
const context = new Context()
|
|
71
|
+
withContext(context, () => {})
|
|
72
|
+
expect(useContext()).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Async propagation suite — only for storage types with true async support
|
|
78
|
+
// (native AsyncLocalStorage). The synchronous browser shim cannot propagate
|
|
79
|
+
// context across await boundaries.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
function asyncPropagationTests(storage: AsyncContextStorage<Context> | undefined) {
|
|
82
|
+
const { withContext, useContext } = createContextAPI<Context>(storage)
|
|
83
|
+
|
|
84
|
+
it('useContext() returns the active context inside async withContext', async () => {
|
|
85
|
+
const context = new Context()
|
|
86
|
+
await withContext(context, async () => {
|
|
87
|
+
await Promise.resolve()
|
|
88
|
+
expect(useContext()).toBe(context)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('context propagates across async boundaries', async () => {
|
|
93
|
+
const context = new Context()
|
|
94
|
+
const results: (Context | null)[] = []
|
|
95
|
+
|
|
96
|
+
await withContext(context, async () => {
|
|
97
|
+
results.push(useContext())
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
99
|
+
results.push(useContext())
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(results).toEqual([context, context])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('nested async contexts (request → event fork)', () => {
|
|
106
|
+
it('full lifecycle: null → outer → inner → outer → null', async () => {
|
|
107
|
+
const requestContext = new Context()
|
|
108
|
+
const eventContext = new Context()
|
|
109
|
+
const snapshots: (Context | null)[] = []
|
|
110
|
+
|
|
111
|
+
snapshots.push(useContext())
|
|
112
|
+
|
|
113
|
+
await withContext(requestContext, async () => {
|
|
114
|
+
snapshots.push(useContext())
|
|
115
|
+
|
|
116
|
+
await withContext(eventContext, async () => {
|
|
117
|
+
snapshots.push(useContext())
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
snapshots.push(useContext())
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
snapshots.push(useContext())
|
|
124
|
+
|
|
125
|
+
expect(snapshots).toEqual([null, requestContext, eventContext, requestContext, null])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('concurrent async tasks each see their own context independently', async () => {
|
|
129
|
+
const requestContext = new Context()
|
|
130
|
+
const eventContext = new Context()
|
|
131
|
+
const requestSnapshots: (Context | null)[] = []
|
|
132
|
+
const eventSnapshots: (Context | null)[] = []
|
|
133
|
+
|
|
134
|
+
await Promise.all([
|
|
135
|
+
withContext(requestContext, async () => {
|
|
136
|
+
requestSnapshots.push(useContext())
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
138
|
+
requestSnapshots.push(useContext())
|
|
139
|
+
}),
|
|
140
|
+
withContext(eventContext, async () => {
|
|
141
|
+
eventSnapshots.push(useContext())
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
143
|
+
eventSnapshots.push(useContext())
|
|
144
|
+
}),
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
expect(requestSnapshots).toEqual([requestContext, requestContext])
|
|
148
|
+
expect(eventSnapshots).toEqual([eventContext, eventContext])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('inner withContext with forked context does not bleed into outer after completion', async () => {
|
|
152
|
+
const requestContext = new Context()
|
|
153
|
+
const eventContext = new Context()
|
|
154
|
+
|
|
155
|
+
await withContext(requestContext, async () => {
|
|
156
|
+
const eventDone = withContext(eventContext, async () => {
|
|
157
|
+
await Promise.resolve()
|
|
158
|
+
expect(useContext()).toBe(eventContext)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(useContext()).toBe(requestContext)
|
|
162
|
+
|
|
163
|
+
await eventDone
|
|
164
|
+
|
|
165
|
+
expect(useContext()).toBe(requestContext)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(useContext()).toBeNull()
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Native AsyncLocalStorage — full async propagation
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
describe('withContext / useContext (native AsyncLocalStorage)', () => {
|
|
177
|
+
sharedContextTests(new NativeALS<Context>())
|
|
178
|
+
asyncPropagationTests(new NativeALS<Context>())
|
|
179
|
+
|
|
180
|
+
// These tests exercise Context.emit() which calls the module-level
|
|
181
|
+
// withContext (always native). They verify the framework integration and
|
|
182
|
+
// are only meaningful with true async propagation.
|
|
183
|
+
describe('typed scopes with event listeners', () => {
|
|
184
|
+
interface AppScope {
|
|
185
|
+
foo: string
|
|
186
|
+
bar: number
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface RequestScope extends AppScope {
|
|
190
|
+
baz: boolean
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
it("event listener receives the emitter's context, not the registration context", async () => {
|
|
194
|
+
const appContext = new Context<AppScope>()
|
|
195
|
+
|
|
196
|
+
let capturedViaALS: Context | null = null
|
|
197
|
+
let capturedViaArg: Context | null = null
|
|
198
|
+
|
|
199
|
+
appContext.on('testEvent', async (listenerContext) => {
|
|
200
|
+
capturedViaArg = listenerContext
|
|
201
|
+
capturedViaALS = useContext()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const requestContext = new Context<RequestScope>().extend(appContext)
|
|
205
|
+
|
|
206
|
+
await withContext(appContext, async () => {
|
|
207
|
+
await withContext(requestContext, async () => {
|
|
208
|
+
await requestContext.emit('testEvent')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(capturedViaArg).toBe(requestContext)
|
|
213
|
+
expect(capturedViaALS).toBe(requestContext)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('useContext() returns the correct typed context across nested scopes', async () => {
|
|
217
|
+
const appContext = new Context<AppScope>()
|
|
218
|
+
appContext.registerValue('foo', 'hello')
|
|
219
|
+
appContext.registerValue('bar', 42)
|
|
220
|
+
|
|
221
|
+
const snapshots: (Context | null)[] = []
|
|
222
|
+
|
|
223
|
+
appContext.on('testEvent', async () => {
|
|
224
|
+
snapshots.push(useContext())
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const requestContext = new Context<RequestScope>().extend(appContext)
|
|
228
|
+
requestContext.registerValue('baz', true)
|
|
229
|
+
|
|
230
|
+
await withContext(appContext, async () => {
|
|
231
|
+
snapshots.push(useContext())
|
|
232
|
+
|
|
233
|
+
await withContext(requestContext, async () => {
|
|
234
|
+
snapshots.push(useContext())
|
|
235
|
+
await requestContext.emit('testEvent')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
snapshots.push(useContext())
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect(snapshots).toEqual([appContext, requestContext, requestContext, appContext])
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('useContext<RequestScope>() narrows the type inside the request scope', async () => {
|
|
245
|
+
const appContext = new Context<AppScope>()
|
|
246
|
+
|
|
247
|
+
let resolvedBaz: boolean | undefined
|
|
248
|
+
|
|
249
|
+
appContext.on('testEvent', async () => {
|
|
250
|
+
const ctx = useContext<Context<RequestScope>>()
|
|
251
|
+
resolvedBaz = ctx?.resolve('baz')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const requestContext = new Context<RequestScope>().extend(appContext)
|
|
255
|
+
requestContext.registerValue('baz', true)
|
|
256
|
+
|
|
257
|
+
await withContext(requestContext, async () => {
|
|
258
|
+
await requestContext.emit('testEvent')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
expect(resolvedBaz).toBe(true)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('strict useContext() succeeds inside a listener because emit sets the context', async () => {
|
|
265
|
+
const appContext = new Context<AppScope>()
|
|
266
|
+
let capturedContext: Context | null = null
|
|
267
|
+
|
|
268
|
+
appContext.on('testEvent', async () => {
|
|
269
|
+
capturedContext = useContext({ strict: true })
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
await appContext.emit('testEvent')
|
|
273
|
+
|
|
274
|
+
expect(capturedContext).toBe(appContext)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Browser shim — synchronous propagation only.
|
|
281
|
+
// Context is available within the synchronous call stack of withContext, but
|
|
282
|
+
// is not propagated across await boundaries. This is expected and correct for
|
|
283
|
+
// browser environments where native async hooks are unavailable.
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
describe('withContext / useContext (browser shim AsyncLocalStorage)', () => {
|
|
286
|
+
sharedContextTests(new ShimALS<Context>())
|
|
287
|
+
|
|
288
|
+
const { withContext, useContext } = createContextAPI<Context>(new ShimALS<Context>())
|
|
289
|
+
|
|
290
|
+
describe('async behavior (sync-only shim)', () => {
|
|
291
|
+
it('context is available at the start of an async fn before any await', async () => {
|
|
292
|
+
const context = new Context()
|
|
293
|
+
let captured: Context | null = null
|
|
294
|
+
|
|
295
|
+
await withContext(context, async () => {
|
|
296
|
+
captured = useContext()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
expect(captured).toBe(context)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('context is not available after an await boundary (expected shim behavior)', async () => {
|
|
303
|
+
const context = new Context()
|
|
304
|
+
let captured: Context | null | 'sentinel' = 'sentinel'
|
|
305
|
+
|
|
306
|
+
await withContext(context, async () => {
|
|
307
|
+
await Promise.resolve()
|
|
308
|
+
captured = useContext()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
expect(captured).toBeNull()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('full lifecycle: null → outer → inner → null → null (sync-only)', async () => {
|
|
315
|
+
const requestContext = new Context()
|
|
316
|
+
const eventContext = new Context()
|
|
317
|
+
const snapshots: (Context | null)[] = []
|
|
318
|
+
|
|
319
|
+
snapshots.push(useContext())
|
|
320
|
+
|
|
321
|
+
await withContext(requestContext, async () => {
|
|
322
|
+
snapshots.push(useContext())
|
|
323
|
+
|
|
324
|
+
await withContext(eventContext, async () => {
|
|
325
|
+
snapshots.push(useContext())
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
snapshots.push(useContext())
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
snapshots.push(useContext())
|
|
332
|
+
|
|
333
|
+
// The shim restores context synchronously when run() exits.
|
|
334
|
+
// After awaiting the inner withContext, the outer context is gone.
|
|
335
|
+
expect(snapshots).toEqual([null, requestContext, eventContext, null, null])
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Default storage — no storage argument; createContextAPI creates its own
|
|
342
|
+
// AsyncLocalStorage internally. In browser builds this resolves to the shim.
|
|
343
|
+
// In Node/Bun (where tests run) it resolves to native AsyncLocalStorage.
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
describe('withContext / useContext (default storage)', () => {
|
|
346
|
+
sharedContextTests(undefined)
|
|
347
|
+
asyncPropagationTests(undefined)
|
|
348
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import type { Context } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal interface for the storage backing `withContext` / `useContext`.
|
|
6
|
+
* Satisfied by Node's native `AsyncLocalStorage` and by the browser shim.
|
|
7
|
+
*/
|
|
8
|
+
export interface AsyncContextStorage<C extends Context = Context> {
|
|
9
|
+
run<R>(store: C, fn: (...args: any[]) => R, ...args: any[]): R
|
|
10
|
+
getStore(): C | undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseContextOptions {
|
|
14
|
+
strict?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a `withContext` / `useContext` pair backed by the given storage.
|
|
19
|
+
*
|
|
20
|
+
* The generic type parameter `C` fixes the context type enforced by both
|
|
21
|
+
* functions, keeping the pair consistent. If no storage is provided, a new
|
|
22
|
+
* `AsyncLocalStorage<C>` is created automatically — in browser builds this
|
|
23
|
+
* resolves to the synchronous shim, so callers don't need to think about it.
|
|
24
|
+
*
|
|
25
|
+
* Pass a custom storage explicitly when you need an alternative
|
|
26
|
+
* implementation (e.g. a test spy or the browser shim in a test environment):
|
|
27
|
+
*
|
|
28
|
+
* @example Default (auto-polyfilled in browser builds)
|
|
29
|
+
* ```ts
|
|
30
|
+
* const { withContext, useContext } = createContextAPI<MyContext>()
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Custom storage (for testing or advanced use)
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { AsyncLocalStorage } from '../shims/async-local-storage'
|
|
36
|
+
* const { withContext, useContext } = createContextAPI<MyContext>(new AsyncLocalStorage())
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createContextAPI<C extends Context = Context>(storage?: AsyncContextStorage<C>) {
|
|
40
|
+
const _storage: AsyncContextStorage<C> = storage ?? new AsyncLocalStorage<C>()
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run `fn` with `context` bound as the active async context. Inside `fn`
|
|
44
|
+
* (and any async work it triggers, including across `await` boundaries),
|
|
45
|
+
* calls to `useContext()` will return this context.
|
|
46
|
+
*
|
|
47
|
+
* Nesting is fully supported: an inner `withContext` creates a child scope
|
|
48
|
+
* that sees its own context; when it exits the parent scope is restored.
|
|
49
|
+
*
|
|
50
|
+
* Internally uses `AsyncLocalStorage` from `node:async_hooks`. In browser
|
|
51
|
+
* environments, a synchronous shim is substituted at build time — context
|
|
52
|
+
* propagates correctly across sequential async flows (event listeners,
|
|
53
|
+
* middleware chains) but not across concurrent async tasks running in
|
|
54
|
+
* parallel.
|
|
55
|
+
*
|
|
56
|
+
* @param context - The context to make active for the duration of `fn`.
|
|
57
|
+
* @param fn - Arbitrary sync or async callback to run inside the context.
|
|
58
|
+
* @returns Whatever `fn` returns.
|
|
59
|
+
*
|
|
60
|
+
* @example Basic usage
|
|
61
|
+
* ```ts
|
|
62
|
+
* await withContext(requestContext, async () => {
|
|
63
|
+
* await eventEmitter.emitAsync(event) // listeners can call useContext()
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Forking the context for an event
|
|
68
|
+
* ```ts
|
|
69
|
+
* await withContext(requestContext, async () => {
|
|
70
|
+
* const eventContext = requestContext.extend()
|
|
71
|
+
* await withContext(eventContext, () => emitter.emitAsync(event))
|
|
72
|
+
* // useContext() here still returns requestContext
|
|
73
|
+
* })
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Creating a context-specific helper (recommended pattern)
|
|
77
|
+
* ```ts
|
|
78
|
+
* // For withContext, define a typed wrapper since TypeScript cannot partially
|
|
79
|
+
* // apply the return-type parameter via an instantiation expression:
|
|
80
|
+
* const withMyContext = <T>(context: MyContext, fn: () => T) =>
|
|
81
|
+
* withContext<T>(context, fn)
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
function withContext<T = unknown>(context: C, fn: () => T): T {
|
|
85
|
+
return _storage.run(context, fn)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return the `Context` currently active in async local storage, or `null`
|
|
90
|
+
* if called outside any `withContext` block.
|
|
91
|
+
*
|
|
92
|
+
* The optional generic parameter `U` narrows the return type to a subclass
|
|
93
|
+
* of `C` when you know the active context is more specific.
|
|
94
|
+
*
|
|
95
|
+
* Backed by `AsyncLocalStorage` on Node/Bun and a synchronous shim in
|
|
96
|
+
* browser builds. See `withContext` for details on browser limitations.
|
|
97
|
+
*
|
|
98
|
+
* @param options.strict - When `true`, throws instead of returning `null`.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const context = useContext() // C | null
|
|
103
|
+
* const context = useContext({ strict: true }) // C (throws if missing)
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example Narrowing to a subtype
|
|
107
|
+
* ```ts
|
|
108
|
+
* const useMyContext = useContext<MyContext>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
function useContext<U extends C = C>(options?: { strict?: false }): U | null
|
|
112
|
+
function useContext<U extends C = C>(options: { strict: true }): U
|
|
113
|
+
function useContext<U extends C = C>(options?: UseContextOptions): U | null {
|
|
114
|
+
const context = (_storage.getStore() ?? null) as U | null
|
|
115
|
+
if (!context && options?.strict) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'useContext() was called outside of an active context. Wrap your code with withContext().'
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
return context
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { withContext, useContext }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Module-level pair backed by the default AsyncLocalStorage.
|
|
127
|
+
// In browser builds the import of node:async_hooks is swapped for the
|
|
128
|
+
// synchronous shim at build time, so this works without any extra config.
|
|
129
|
+
export const { withContext, useContext } = createContextAPI<Context>()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Context } from './context'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
> {
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Inject dependencies directly into the constructor of the class that needs them.
|
|
5
|
+
*/
|
|
6
|
+
export class ContextConsumer<C extends Context = Context, A extends any[] = any[]> {
|
|
7
7
|
constructor(protected readonly context: C, ...args: A) {}
|
|
8
8
|
}
|