@declaro/core 2.0.0-y.0 → 2.1.1

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.
Files changed (266) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +203 -0
  3. package/dist/browser/index.js +28 -0
  4. package/dist/browser/index.js.map +133 -0
  5. package/dist/browser/scope/index.js +3 -0
  6. package/dist/browser/scope/index.js.map +9 -0
  7. package/dist/bun/index.js +19011 -0
  8. package/dist/bun/index.js.map +132 -0
  9. package/dist/bun/scope/index.js +4 -0
  10. package/dist/bun/scope/index.js.map +9 -0
  11. package/dist/node/index.cjs +19039 -0
  12. package/dist/node/index.cjs.map +132 -0
  13. package/dist/node/index.js +19010 -0
  14. package/dist/node/index.js.map +132 -0
  15. package/dist/node/scope/index.cjs +69 -0
  16. package/dist/node/scope/index.cjs.map +9 -0
  17. package/dist/node/scope/index.js +3 -0
  18. package/dist/node/scope/index.js.map +9 -0
  19. package/dist/ts/app/app-context.d.ts +9 -0
  20. package/dist/ts/app/app-context.d.ts.map +1 -0
  21. package/dist/ts/app/app-lifecycle.d.ts +6 -0
  22. package/dist/ts/app/app-lifecycle.d.ts.map +1 -0
  23. package/dist/ts/app/app.d.ts +24 -0
  24. package/dist/ts/app/app.d.ts.map +1 -0
  25. package/dist/{app → ts/app}/index.d.ts +1 -0
  26. package/dist/ts/app/index.d.ts.map +1 -0
  27. package/dist/ts/application/create-request-context.d.ts +4 -0
  28. package/dist/ts/application/create-request-context.d.ts.map +1 -0
  29. package/dist/ts/application/create-request-context.test.d.ts +2 -0
  30. package/dist/ts/application/create-request-context.test.d.ts.map +1 -0
  31. package/dist/ts/application/use-declaro.d.ts +3 -0
  32. package/dist/ts/application/use-declaro.d.ts.map +1 -0
  33. package/dist/{auth → ts/auth}/permission-validator.d.ts +1 -0
  34. package/dist/ts/auth/permission-validator.d.ts.map +1 -0
  35. package/dist/ts/auth/permission-validator.test.d.ts +2 -0
  36. package/dist/ts/auth/permission-validator.test.d.ts.map +1 -0
  37. package/dist/ts/context/async-context.d.ts +54 -0
  38. package/dist/ts/context/async-context.d.ts.map +1 -0
  39. package/dist/ts/context/async-context.test.d.ts +2 -0
  40. package/dist/ts/context/async-context.test.d.ts.map +1 -0
  41. package/dist/{context → ts/context}/context-consumer.d.ts +4 -0
  42. package/dist/ts/context/context-consumer.d.ts.map +1 -0
  43. package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
  44. package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
  45. package/dist/ts/context/context.d.ts +452 -0
  46. package/dist/ts/context/context.d.ts.map +1 -0
  47. package/dist/ts/context/context.test.d.ts +2 -0
  48. package/dist/ts/context/context.test.d.ts.map +1 -0
  49. package/dist/ts/context/legacy-context.test.d.ts +2 -0
  50. package/dist/ts/context/legacy-context.test.d.ts.map +1 -0
  51. package/dist/{context → ts/context}/validators.d.ts +2 -1
  52. package/dist/ts/context/validators.d.ts.map +1 -0
  53. package/dist/ts/dataflow/index.d.ts +2 -0
  54. package/dist/ts/dataflow/index.d.ts.map +1 -0
  55. package/dist/ts/dataflow/objects.d.ts +7 -0
  56. package/dist/ts/dataflow/objects.d.ts.map +1 -0
  57. package/dist/ts/dataflow/objects.test.d.ts +2 -0
  58. package/dist/ts/dataflow/objects.test.d.ts.map +1 -0
  59. package/dist/{errors → ts/errors}/errors.d.ts +16 -3
  60. package/dist/ts/errors/errors.d.ts.map +1 -0
  61. package/dist/ts/events/event-manager.d.ts +19 -0
  62. package/dist/ts/events/event-manager.d.ts.map +1 -0
  63. package/dist/ts/events/event-manager.spec.d.ts +2 -0
  64. package/dist/ts/events/event-manager.spec.d.ts.map +1 -0
  65. package/dist/ts/events/index.d.ts +2 -0
  66. package/dist/ts/events/index.d.ts.map +1 -0
  67. package/dist/ts/http/headers.d.ts +21 -0
  68. package/dist/ts/http/headers.d.ts.map +1 -0
  69. package/dist/ts/http/headers.spec.d.ts +2 -0
  70. package/dist/ts/http/headers.spec.d.ts.map +1 -0
  71. package/dist/ts/http/request-context.d.ts +17 -0
  72. package/dist/ts/http/request-context.d.ts.map +1 -0
  73. package/dist/ts/http/request-context.spec.d.ts +2 -0
  74. package/dist/ts/http/request-context.spec.d.ts.map +1 -0
  75. package/dist/ts/http/request.d.ts +31 -0
  76. package/dist/ts/http/request.d.ts.map +1 -0
  77. package/dist/ts/http/request.spec.d.ts +2 -0
  78. package/dist/ts/http/request.spec.d.ts.map +1 -0
  79. package/dist/{http → ts/http}/url.d.ts +5 -4
  80. package/dist/ts/http/url.d.ts.map +1 -0
  81. package/dist/ts/http/url.spec.d.ts +2 -0
  82. package/dist/ts/http/url.spec.d.ts.map +1 -0
  83. package/dist/ts/index.d.ts +47 -0
  84. package/dist/ts/index.d.ts.map +1 -0
  85. package/dist/{pipelines → ts/pipelines}/index.d.ts +1 -0
  86. package/dist/ts/pipelines/index.d.ts.map +1 -0
  87. package/dist/{pipelines → ts/pipelines}/pipeline-action.d.ts +1 -0
  88. package/dist/ts/pipelines/pipeline-action.d.ts.map +1 -0
  89. package/dist/ts/pipelines/pipeline-action.test.d.ts +2 -0
  90. package/dist/ts/pipelines/pipeline-action.test.d.ts.map +1 -0
  91. package/dist/{pipelines → ts/pipelines}/pipeline.d.ts +3 -2
  92. package/dist/ts/pipelines/pipeline.d.ts.map +1 -0
  93. package/dist/ts/pipelines/pipeline.test.d.ts +2 -0
  94. package/dist/ts/pipelines/pipeline.test.d.ts.map +1 -0
  95. package/dist/ts/schema/json-schema.d.ts +12 -0
  96. package/dist/ts/schema/json-schema.d.ts.map +1 -0
  97. package/dist/ts/schema/labels.d.ts +14 -0
  98. package/dist/ts/schema/labels.d.ts.map +1 -0
  99. package/dist/ts/schema/model-schema.d.ts +75 -0
  100. package/dist/ts/schema/model-schema.d.ts.map +1 -0
  101. package/dist/ts/schema/model-schema.test.d.ts +2 -0
  102. package/dist/ts/schema/model-schema.test.d.ts.map +1 -0
  103. package/dist/ts/schema/model.d.ts +35 -0
  104. package/dist/ts/schema/model.d.ts.map +1 -0
  105. package/dist/ts/schema/schema-mixin.d.ts +24 -0
  106. package/dist/ts/schema/schema-mixin.d.ts.map +1 -0
  107. package/dist/ts/schema/test/mock-model.d.ts +8 -0
  108. package/dist/ts/schema/test/mock-model.d.ts.map +1 -0
  109. package/dist/ts/scope/index.d.ts +34 -0
  110. package/dist/ts/scope/index.d.ts.map +1 -0
  111. package/dist/ts/shared/utils/action-descriptor.d.ts +28 -0
  112. package/dist/ts/shared/utils/action-descriptor.d.ts.map +1 -0
  113. package/dist/ts/shared/utils/action-descriptor.test.d.ts +2 -0
  114. package/dist/ts/shared/utils/action-descriptor.test.d.ts.map +1 -0
  115. package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
  116. package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
  117. package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
  118. package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
  119. package/dist/ts/shims/async-local-storage.d.ts +36 -0
  120. package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
  121. package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
  122. package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
  123. package/dist/{timing.d.ts → ts/timing.d.ts} +1 -0
  124. package/dist/ts/timing.d.ts.map +1 -0
  125. package/dist/{typescript → ts/typescript}/arrays.d.ts +1 -0
  126. package/dist/ts/typescript/arrays.d.ts.map +1 -0
  127. package/dist/{typescript → ts/typescript}/baseModel.d.ts +1 -0
  128. package/dist/ts/typescript/baseModel.d.ts.map +1 -0
  129. package/dist/{typescript → ts/typescript}/classes.d.ts +1 -0
  130. package/dist/ts/typescript/classes.d.ts.map +1 -0
  131. package/dist/{typescript → ts/typescript}/constant-manipulation/snake-case.d.ts +1 -0
  132. package/dist/ts/typescript/constant-manipulation/snake-case.d.ts.map +1 -0
  133. package/dist/{typescript → ts/typescript}/errors.d.ts +1 -0
  134. package/dist/ts/typescript/errors.d.ts.map +1 -0
  135. package/dist/ts/typescript/fetch.d.ts +3 -0
  136. package/dist/ts/typescript/fetch.d.ts.map +1 -0
  137. package/dist/{typescript → ts/typescript}/generics.d.ts +1 -0
  138. package/dist/ts/typescript/generics.d.ts.map +1 -0
  139. package/dist/{typescript → ts/typescript}/index.d.ts +1 -0
  140. package/dist/ts/typescript/index.d.ts.map +1 -0
  141. package/dist/ts/typescript/objects.d.ts +26 -0
  142. package/dist/ts/typescript/objects.d.ts.map +1 -0
  143. package/dist/{typescript → ts/typescript}/promises.d.ts +1 -0
  144. package/dist/ts/typescript/promises.d.ts.map +1 -0
  145. package/dist/{validation → ts/validation}/index.d.ts +1 -0
  146. package/dist/ts/validation/index.d.ts.map +1 -0
  147. package/dist/{validation → ts/validation}/validation.d.ts +1 -0
  148. package/dist/ts/validation/validation.d.ts.map +1 -0
  149. package/dist/{validation → ts/validation}/validator.d.ts +1 -0
  150. package/dist/ts/validation/validator.d.ts.map +1 -0
  151. package/dist/ts/validation/validator.test.d.ts +2 -0
  152. package/dist/ts/validation/validator.test.d.ts.map +1 -0
  153. package/package.json +46 -13
  154. package/src/app/app-context.ts +4 -5
  155. package/src/app/app-lifecycle.ts +4 -3
  156. package/src/app/app.ts +7 -5
  157. package/src/application/create-request-context.test.ts +345 -0
  158. package/src/application/create-request-context.ts +19 -0
  159. package/src/application/use-declaro.ts +27 -0
  160. package/src/auth/permission-validator.test.ts +238 -2
  161. package/src/auth/permission-validator.ts +3 -3
  162. package/src/context/async-context.test.ts +348 -0
  163. package/src/context/async-context.ts +129 -0
  164. package/src/context/context-consumer.ts +4 -4
  165. package/src/context/context.circular-deps.test.ts +1047 -0
  166. package/src/context/context.test.ts +420 -3
  167. package/src/context/context.ts +590 -87
  168. package/src/context/legacy-context.test.ts +9 -9
  169. package/src/dataflow/objects.test.ts +7 -7
  170. package/src/dataflow/objects.ts +10 -9
  171. package/src/errors/errors.ts +19 -3
  172. package/src/events/event-manager.spec.ts +129 -0
  173. package/src/events/event-manager.ts +25 -14
  174. package/src/http/headers.ts +17 -2
  175. package/src/http/request-context.ts +24 -15
  176. package/src/http/request.ts +27 -6
  177. package/src/http/url.ts +3 -3
  178. package/src/index.ts +34 -3
  179. package/src/pipelines/pipeline.test.ts +11 -9
  180. package/src/schema/json-schema.ts +16 -0
  181. package/src/schema/labels.ts +23 -23
  182. package/src/schema/model-schema.test.ts +282 -0
  183. package/src/schema/model-schema.ts +197 -0
  184. package/src/schema/model.ts +143 -0
  185. package/src/schema/schema-mixin.ts +51 -0
  186. package/src/schema/test/mock-model.ts +19 -0
  187. package/src/scope/index.ts +33 -0
  188. package/src/shared/utils/action-descriptor.test.ts +182 -0
  189. package/src/shared/utils/action-descriptor.ts +102 -0
  190. package/src/shared/utils/schema-utils.test.ts +33 -0
  191. package/src/shared/utils/schema-utils.ts +17 -0
  192. package/src/shims/async-local-storage.test.ts +258 -0
  193. package/src/shims/async-local-storage.ts +82 -0
  194. package/src/typescript/objects.ts +32 -1
  195. package/src/validation/validator.test.ts +12 -20
  196. package/dist/app/app-context.d.ts +0 -8
  197. package/dist/app/app-lifecycle.d.ts +0 -4
  198. package/dist/app/app.d.ts +0 -22
  199. package/dist/auth/permission-validator.test.d.ts +0 -1
  200. package/dist/context/context.d.ts +0 -161
  201. package/dist/context/context.test.d.ts +0 -1
  202. package/dist/context/legacy-context.test.d.ts +0 -1
  203. package/dist/dataflow/index.d.ts +0 -1
  204. package/dist/dataflow/objects.d.ts +0 -5
  205. package/dist/dataflow/objects.test.d.ts +0 -1
  206. package/dist/events/event-manager.d.ts +0 -16
  207. package/dist/events/event-manager.spec.d.ts +0 -1
  208. package/dist/events/index.d.ts +0 -1
  209. package/dist/helpers/index.d.ts +0 -1
  210. package/dist/helpers/ucfirst.d.ts +0 -1
  211. package/dist/http/headers.d.ts +0 -4
  212. package/dist/http/headers.spec.d.ts +0 -1
  213. package/dist/http/request-context.d.ts +0 -12
  214. package/dist/http/request-context.spec.d.ts +0 -1
  215. package/dist/http/request.d.ts +0 -8
  216. package/dist/http/request.spec.d.ts +0 -1
  217. package/dist/http/url.spec.d.ts +0 -1
  218. package/dist/index.d.ts +0 -19
  219. package/dist/pipelines/pipeline-action.test.d.ts +0 -1
  220. package/dist/pipelines/pipeline.test.d.ts +0 -1
  221. package/dist/pkg.cjs +0 -30
  222. package/dist/pkg.mjs +0 -56612
  223. package/dist/schema/application.d.ts +0 -83
  224. package/dist/schema/application.test.d.ts +0 -1
  225. package/dist/schema/define-model.d.ts +0 -10
  226. package/dist/schema/define-model.test.d.ts +0 -1
  227. package/dist/schema/formats.d.ts +0 -10
  228. package/dist/schema/index.d.ts +0 -10
  229. package/dist/schema/labels.d.ts +0 -13
  230. package/dist/schema/labels.test.d.ts +0 -1
  231. package/dist/schema/module.d.ts +0 -7
  232. package/dist/schema/module.test.d.ts +0 -1
  233. package/dist/schema/properties.d.ts +0 -19
  234. package/dist/schema/response.d.ts +0 -31
  235. package/dist/schema/response.test.d.ts +0 -1
  236. package/dist/schema/supported-types.d.ts +0 -12
  237. package/dist/schema/supported-types.test.d.ts +0 -1
  238. package/dist/schema/transform-model.d.ts +0 -4
  239. package/dist/schema/transform-model.test.d.ts +0 -1
  240. package/dist/schema/types.d.ts +0 -95
  241. package/dist/schema/types.test.d.ts +0 -1
  242. package/dist/typescript/fetch.d.ts +0 -2
  243. package/dist/typescript/objects.d.ts +0 -12
  244. package/dist/validation/validator.test.d.ts +0 -1
  245. package/src/helpers/index.ts +0 -1
  246. package/src/helpers/ucfirst.ts +0 -3
  247. package/src/schema/application.test.ts +0 -286
  248. package/src/schema/application.ts +0 -150
  249. package/src/schema/define-model.test.ts +0 -81
  250. package/src/schema/define-model.ts +0 -50
  251. package/src/schema/formats.ts +0 -23
  252. package/src/schema/index.ts +0 -10
  253. package/src/schema/labels.test.ts +0 -60
  254. package/src/schema/module.test.ts +0 -39
  255. package/src/schema/module.ts +0 -6
  256. package/src/schema/properties.ts +0 -40
  257. package/src/schema/response.test.ts +0 -101
  258. package/src/schema/response.ts +0 -93
  259. package/src/schema/supported-types.test.ts +0 -20
  260. package/src/schema/supported-types.ts +0 -15
  261. package/src/schema/transform-model.test.ts +0 -31
  262. package/src/schema/transform-model.ts +0 -24
  263. package/src/schema/types.test.ts +0 -28
  264. package/src/schema/types.ts +0 -163
  265. package/tsconfig.json +0 -11
  266. package/vite.config.ts +0 -24
@@ -0,0 +1,258 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { AsyncLocalStorage } from './async-local-storage'
3
+
4
+ describe('AsyncLocalStorage shim', () => {
5
+ describe('run()', () => {
6
+ it('returns the fn result synchronously', () => {
7
+ const als = new AsyncLocalStorage<number>()
8
+ const result = als.run(1, () => 42)
9
+ expect(result).toBe(42)
10
+ })
11
+
12
+ it('returns a Promise when fn is async', async () => {
13
+ const als = new AsyncLocalStorage<string>()
14
+ const result = await als.run('ctx', async () => 'hello')
15
+ expect(result).toBe('hello')
16
+ })
17
+
18
+ it('forwards extra args to fn', () => {
19
+ const als = new AsyncLocalStorage<string>()
20
+ const result = als.run('ctx', (a: number, b: number) => a + b, 3, 4)
21
+ expect(result).toBe(7)
22
+ })
23
+
24
+ it('getStore() returns the active store inside run()', () => {
25
+ const als = new AsyncLocalStorage<string>()
26
+ als.run('hello', () => {
27
+ expect(als.getStore()).toBe('hello')
28
+ })
29
+ })
30
+
31
+ it('getStore() returns undefined outside of run()', () => {
32
+ const als = new AsyncLocalStorage<string>()
33
+ expect(als.getStore()).toBeUndefined()
34
+ })
35
+
36
+ it('store is not visible after run() completes', () => {
37
+ const als = new AsyncLocalStorage<string>()
38
+ als.run('hello', () => {})
39
+ expect(als.getStore()).toBeUndefined()
40
+ })
41
+
42
+ it('nested run() calls use the innermost store', () => {
43
+ const als = new AsyncLocalStorage<string>()
44
+
45
+ als.run('outer', () => {
46
+ expect(als.getStore()).toBe('outer')
47
+
48
+ als.run('inner', () => {
49
+ expect(als.getStore()).toBe('inner')
50
+ })
51
+
52
+ expect(als.getStore()).toBe('outer')
53
+ })
54
+ })
55
+
56
+ it('restores prior store after nested run() completes', () => {
57
+ const als = new AsyncLocalStorage<string>()
58
+ als.run('outer', () => {
59
+ als.run('inner', () => {})
60
+ expect(als.getStore()).toBe('outer')
61
+ })
62
+ expect(als.getStore()).toBeUndefined()
63
+ })
64
+
65
+ it('full lifecycle: undefined → outer → inner → outer → undefined', () => {
66
+ const als = new AsyncLocalStorage<string>()
67
+ const snapshots: (string | undefined)[] = []
68
+
69
+ snapshots.push(als.getStore())
70
+ als.run('outer', () => {
71
+ snapshots.push(als.getStore())
72
+ als.run('inner', () => {
73
+ snapshots.push(als.getStore())
74
+ })
75
+ snapshots.push(als.getStore())
76
+ })
77
+ snapshots.push(als.getStore())
78
+
79
+ expect(snapshots).toEqual([undefined, 'outer', 'inner', 'outer', undefined])
80
+ })
81
+
82
+ it('restores store even when fn throws', () => {
83
+ const als = new AsyncLocalStorage<string>()
84
+ expect(() =>
85
+ als.run('ctx', () => {
86
+ throw new Error('boom')
87
+ }),
88
+ ).toThrow('boom')
89
+ expect(als.getStore()).toBeUndefined()
90
+ })
91
+
92
+ it('multiple ALS instances are isolated from each other', () => {
93
+ const als1 = new AsyncLocalStorage<string>()
94
+ const als2 = new AsyncLocalStorage<number>()
95
+
96
+ als1.run('hello', () => {
97
+ als2.run(42, () => {
98
+ expect(als1.getStore()).toBe('hello')
99
+ expect(als2.getStore()).toBe(42)
100
+ })
101
+ expect(als1.getStore()).toBe('hello')
102
+ expect(als2.getStore()).toBeUndefined()
103
+ })
104
+ })
105
+ })
106
+
107
+ describe('async usage', () => {
108
+ it('store is accessible at the start of an async fn (before any await)', async () => {
109
+ const als = new AsyncLocalStorage<string>()
110
+ let captured: string | undefined
111
+
112
+ await als.run('ctx', async () => {
113
+ // No await yet — still in the synchronous call stack of run()
114
+ captured = als.getStore()
115
+ })
116
+
117
+ expect(captured).toBe('ctx')
118
+ })
119
+
120
+ it('resolves the returned Promise correctly', async () => {
121
+ const als = new AsyncLocalStorage<string>()
122
+ const result = await als.run('ctx', async () => 'resolved')
123
+ expect(result).toBe('resolved')
124
+ })
125
+
126
+ it('propagates rejections correctly', async () => {
127
+ const als = new AsyncLocalStorage<string>()
128
+ await expect(
129
+ als.run('ctx', async () => {
130
+ throw new Error('async boom')
131
+ }),
132
+ ).rejects.toThrow('async boom')
133
+ })
134
+
135
+ it('restores store after an async fn resolves', async () => {
136
+ const als = new AsyncLocalStorage<string>()
137
+ await als.run('ctx', async () => {})
138
+ expect(als.getStore()).toBeUndefined()
139
+ })
140
+
141
+ // The shim uses synchronous save/restore. The finally block in run()
142
+ // fires as soon as the async fn returns its Promise — before any
143
+ // awaited microtasks resume. Context is therefore not visible after an
144
+ // await boundary. Native AsyncLocalStorage (Node/Bun) does not have
145
+ // this limitation.
146
+ it('does NOT propagate store across await boundaries (synchronous shim limitation)', async () => {
147
+ const als = new AsyncLocalStorage<string>()
148
+ let captured: string | undefined = 'sentinel'
149
+
150
+ await als.run('ctx', async () => {
151
+ await Promise.resolve()
152
+ captured = als.getStore()
153
+ })
154
+
155
+ expect(captured).toBeUndefined()
156
+ })
157
+
158
+ it('does NOT isolate concurrent async tasks (synchronous shim limitation)', async () => {
159
+ const als = new AsyncLocalStorage<string>()
160
+ const results: (string | undefined)[] = []
161
+
162
+ await Promise.all([
163
+ als.run('task-a', async () => {
164
+ await new Promise<void>((r) => setTimeout(r, 10))
165
+ results.push(als.getStore())
166
+ }),
167
+ als.run('task-b', async () => {
168
+ await new Promise<void>((r) => setTimeout(r, 5))
169
+ results.push(als.getStore())
170
+ }),
171
+ ])
172
+
173
+ expect(results).toEqual([undefined, undefined])
174
+ })
175
+ })
176
+
177
+ describe('exit()', () => {
178
+ it('clears the store for the duration of fn', () => {
179
+ const als = new AsyncLocalStorage<string>()
180
+ als.run('ctx', () => {
181
+ als.exit(() => {
182
+ expect(als.getStore()).toBeUndefined()
183
+ })
184
+ })
185
+ })
186
+
187
+ it('restores the store after exit() completes', () => {
188
+ const als = new AsyncLocalStorage<string>()
189
+ als.run('ctx', () => {
190
+ als.exit(() => {})
191
+ expect(als.getStore()).toBe('ctx')
192
+ })
193
+ })
194
+
195
+ it('restores store even when fn throws', () => {
196
+ const als = new AsyncLocalStorage<string>()
197
+ als.run('ctx', () => {
198
+ expect(() =>
199
+ als.exit(() => {
200
+ throw new Error('boom')
201
+ }),
202
+ ).toThrow('boom')
203
+ expect(als.getStore()).toBe('ctx')
204
+ })
205
+ })
206
+ })
207
+
208
+ describe('enterWith()', () => {
209
+ it('sets the store imperatively', () => {
210
+ const als = new AsyncLocalStorage<string>()
211
+ als.enterWith('hello')
212
+ expect(als.getStore()).toBe('hello')
213
+ })
214
+
215
+ it('can be overwritten by another enterWith()', () => {
216
+ const als = new AsyncLocalStorage<string>()
217
+ als.enterWith('first')
218
+ als.enterWith('second')
219
+ expect(als.getStore()).toBe('second')
220
+ })
221
+ })
222
+
223
+ describe('disable()', () => {
224
+ it('clears the store', () => {
225
+ const als = new AsyncLocalStorage<string>()
226
+ als.enterWith('hello')
227
+ als.disable()
228
+ expect(als.getStore()).toBeUndefined()
229
+ })
230
+
231
+ it('has no effect when store is already empty', () => {
232
+ const als = new AsyncLocalStorage<string>()
233
+ als.disable()
234
+ expect(als.getStore()).toBeUndefined()
235
+ })
236
+ })
237
+
238
+ describe('AsyncLocalStorage.bind()', () => {
239
+ it('returns the function unchanged', () => {
240
+ const fn = () => 42
241
+ expect(AsyncLocalStorage.bind(fn)).toBe(fn)
242
+ })
243
+ })
244
+
245
+ describe('AsyncLocalStorage.snapshot()', () => {
246
+ it('returns a function that calls fn with provided args', () => {
247
+ const run = AsyncLocalStorage.snapshot()
248
+ const result = run((a: number, b: number) => a + b, 3, 4)
249
+ expect(result).toBe(7)
250
+ })
251
+
252
+ it('each call to snapshot() returns an independent executor', () => {
253
+ const run1 = AsyncLocalStorage.snapshot()
254
+ const run2 = AsyncLocalStorage.snapshot()
255
+ expect(run1).not.toBe(run2)
256
+ })
257
+ })
258
+ })
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Browser polyfill for Node's AsyncLocalStorage.
3
+ *
4
+ * ## How it works
5
+ *
6
+ * Each AsyncLocalStorage instance has a unique Symbol as its "column" ID.
7
+ * The currently active state is a single `Map<symbol, unknown>` called a
8
+ * _frame_, where each column holds the value for one ALS instance.
9
+ *
10
+ * `run()` creates a new frame (a shallow copy of the parent frame with this
11
+ * instance's value set), makes it active for the duration of `fn`, then
12
+ * restores the previous frame in a `finally` block.
13
+ *
14
+ * Multiple ALS instances are fully isolated from each other within the same
15
+ * frame: `als1.run()` only writes to its own column and does not affect any
16
+ * other instance's column.
17
+ *
18
+ * ## Browser limitation
19
+ *
20
+ * Because browsers have no native async-context hook, the frame is propagated
21
+ * only within the synchronous call stack of `fn`. Any code that runs after an
22
+ * `await` boundary cannot see the frame that was active when the `await` was
23
+ * encountered. For true async propagation in the browser, use Zone.js or the
24
+ * forthcoming `AsyncContext` TC39 API.
25
+ */
26
+
27
+ // A frame maps ALS instance IDs → their stored values.
28
+ // All concurrently active ALS instances share one frame object.
29
+ type Frame = Map<symbol, unknown>
30
+
31
+ let _activeFrame: Frame | undefined = undefined
32
+
33
+ export class AsyncLocalStorage<T = any> {
34
+ // Unique identity for this ALS instance within a frame.
35
+ readonly #id: symbol = Symbol('AsyncLocalStorage')
36
+
37
+ getStore(): T | undefined {
38
+ return _activeFrame?.get(this.#id) as T | undefined
39
+ }
40
+
41
+ run<R>(store: T, fn: (...args: any[]) => R, ...args: any[]): R {
42
+ const prev = _activeFrame
43
+ _activeFrame = new Map(prev)
44
+ _activeFrame.set(this.#id, store)
45
+ try {
46
+ return fn(...args)
47
+ } finally {
48
+ _activeFrame = prev
49
+ }
50
+ }
51
+
52
+ exit<R>(fn: (...args: any[]) => R, ...args: any[]): R {
53
+ const prev = _activeFrame
54
+ _activeFrame = new Map(prev)
55
+ _activeFrame.delete(this.#id)
56
+ try {
57
+ return fn(...args)
58
+ } finally {
59
+ _activeFrame = prev
60
+ }
61
+ }
62
+
63
+ enterWith(store: T): void {
64
+ const frame = new Map(_activeFrame)
65
+ frame.set(this.#id, store)
66
+ _activeFrame = frame
67
+ }
68
+
69
+ disable(): void {
70
+ const frame = new Map(_activeFrame)
71
+ frame.delete(this.#id)
72
+ _activeFrame = frame
73
+ }
74
+
75
+ static bind<F extends (...args: any[]) => any>(fn: F): F {
76
+ return fn
77
+ }
78
+
79
+ static snapshot(): <R>(fn: (...args: any[]) => R, ...args: any[]) => R {
80
+ return (fn, ...args) => fn(...args)
81
+ }
82
+ }
@@ -11,5 +11,36 @@ export type DeepPartial<T> = T extends object
11
11
  * Merge two object types without using an intersection type. Intersection types preserve the original types of the objects causing confusion, while this type will merge the types of the objects.
12
12
  */
13
13
  export type Merge<A, B> = {
14
- [key in keyof A | keyof B]: key extends keyof B ? B[key] : key extends keyof A ? A[key] : never
14
+ [K in keyof A | keyof B]: K extends keyof B
15
+ ? B[K] extends object[] // Support for arrays
16
+ ? K extends keyof A
17
+ ? A[K] extends object[]
18
+ ? Merge<A[K], B[K]> // If both are arrays, merge them
19
+ : B[K] // If only B is an array, use B
20
+ : B[K]
21
+ : B[K] extends object
22
+ ? K extends keyof A
23
+ ? A[K] extends object
24
+ ? Merge<A[K], B[K]>
25
+ : B[K]
26
+ : B[K]
27
+ : B[K]
28
+ : K extends keyof A
29
+ ? A[K]
30
+ : never
15
31
  }
32
+
33
+ /**
34
+ * Shallow merge two object types, replacing types of properties instead of merging them.
35
+ * Properties in B will completely replace properties in A.
36
+ */
37
+ export type ShallowMerge<A extends object, B extends object> = {
38
+ [K in UniqueKeys<keyof A, keyof B>]: A[K]
39
+ } & {
40
+ [K in keyof B]: B[K]
41
+ }
42
+
43
+ /**
44
+ * Extract keys from A that are not present in B
45
+ */
46
+ export type UniqueKeys<A, B> = A extends B ? never : A
@@ -1,5 +1,5 @@
1
1
  import { validate, validateAny } from './validator'
2
- import { describe, it, vi } from 'vitest'
2
+ import { describe, it, vi, expect } from 'vitest'
3
3
 
4
4
  async function sleep(duration: number) {
5
5
  return new Promise((resolve, reject) => {
@@ -10,31 +10,25 @@ async function sleep(duration: number) {
10
10
  }
11
11
 
12
12
  describe('Validator', () => {
13
- it('Should validate with valid property', async ({ expect }) => {
13
+ it('Should validate with valid property', async () => {
14
14
  const value = {
15
15
  message: 'Hello World',
16
16
  }
17
- const valid = await validate(
18
- value,
19
- (val) => val.message === 'Hello World',
20
- ).valid
17
+ const valid = await validate(value, (val) => val.message === 'Hello World').valid
21
18
 
22
19
  expect(valid).toBeTruthy()
23
20
  })
24
21
 
25
- it('Should validate with invalid property', async ({ expect }) => {
22
+ it('Should validate with invalid property', async () => {
26
23
  const value = {
27
24
  message: 'Hello Mr. Anderson',
28
25
  }
29
- const valid = await validate(
30
- value,
31
- (val) => val.message === 'Hello World',
32
- ).valid
26
+ const valid = await validate(value, (val) => val.message === 'Hello World').valid
33
27
 
34
28
  expect(valid).toBeFalsy()
35
29
  })
36
30
 
37
- it('Should validate with onValid callback', async ({ expect }) => {
31
+ it('Should validate with onValid callback', async () => {
38
32
  const valid = vi.fn((val) => {
39
33
  expect(val).toBe(42)
40
34
  })
@@ -54,7 +48,7 @@ describe('Validator', () => {
54
48
  expect(invalid.mock.calls.length).toBe(0)
55
49
  })
56
50
 
57
- it('Should validate with onInvalid callback', async ({ expect }) => {
51
+ it('Should validate with onInvalid callback', async () => {
58
52
  const valid = vi.fn((val) => {
59
53
  expect(val).toBe(42)
60
54
  })
@@ -73,7 +67,7 @@ describe('Validator', () => {
73
67
  expect(invalid.mock.calls.length).toBe(1)
74
68
  })
75
69
 
76
- it('Should validate multiple validators', async ({ expect }) => {
70
+ it('Should validate multiple validators', async () => {
77
71
  const value = {
78
72
  message: 'Hello Mr. Anderson',
79
73
  }
@@ -87,9 +81,7 @@ describe('Validator', () => {
87
81
  expect(valid).toBeTruthy()
88
82
  })
89
83
 
90
- it('Should invalidate if one of multiple validators fails', async ({
91
- expect,
92
- }) => {
84
+ it('Should invalidate if one of multiple validators fails', async () => {
93
85
  const value = {
94
86
  message: 'Hello Mr. Anderson',
95
87
  }
@@ -103,7 +95,7 @@ describe('Validator', () => {
103
95
  expect(valid).toBeFalsy()
104
96
  })
105
97
 
106
- it('Should be able to validate an any strategy', async ({ expect }) => {
98
+ it('Should be able to validate an any strategy', async () => {
107
99
  const value = {
108
100
  message: 'Hello Mr. Anderson',
109
101
  }
@@ -117,7 +109,7 @@ describe('Validator', () => {
117
109
  expect(valid).toBeTruthy()
118
110
  })
119
111
 
120
- it('Should be able to invalidate an any strategy', async ({ expect }) => {
112
+ it('Should be able to invalidate an any strategy', async () => {
121
113
  const value = {
122
114
  message: 'Hello Mr. Anderson',
123
115
  }
@@ -131,7 +123,7 @@ describe('Validator', () => {
131
123
  expect(valid).toBeFalsy()
132
124
  })
133
125
 
134
- it('Should validate an async validator', async ({ expect }) => {
126
+ it('Should validate an async validator', async () => {
135
127
  const value = {
136
128
  message: 'Hello World',
137
129
  }
@@ -1,8 +0,0 @@
1
- import { Context } from '../context/context';
2
- /**
3
- * Get the name of the app making the current request via header x-app. (returns "default" when no app was provided).
4
- *
5
- * @param context
6
- * @returns the name of the app in a string.
7
- */
8
- export declare function useApp(context: Context): string;
@@ -1,4 +0,0 @@
1
- import { Context, ContextListener } from '../context/context';
2
- export declare function onInit(context: Context, listener: ContextListener): void;
3
- export declare function onStart(context: Context, listener: ContextListener): void;
4
- export declare function onDestroy(context: Context, listener: ContextListener): void;
package/dist/app/app.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import { Context, ContextMiddleware, ContextListener } from '../context/context';
2
- export type AppConfig = {
3
- context?: Context;
4
- init?: ContextMiddleware;
5
- start?: ContextMiddleware;
6
- destroy?: ContextMiddleware;
7
- };
8
- export declare class App {
9
- readonly context: Context;
10
- constructor(context: Context);
11
- static Events: {
12
- Init: string;
13
- Start: string;
14
- Destroy: string;
15
- };
16
- init(): Promise<void>;
17
- onInit(listener: ContextListener): this;
18
- start(): Promise<void>;
19
- onStart(listener: ContextListener): void;
20
- destroy(): Promise<void>;
21
- onDestroy(listener: ContextListener): void;
22
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,161 +0,0 @@
1
- import { Class, PromiseOrValue, UnwrapPromise } from '../typescript';
2
- import { Validator } from '../validation';
3
- import { ContextConsumer } from './context-consumer';
4
- export type AppScope = {};
5
- export type RequestScope = {};
6
- export type AppContext = Context<AppScope>;
7
- export type RequestContext = Context<RequestScope>;
8
- export type ContextMiddleware = (context: Context) => any | Promise<any>;
9
- export type ContextState<TContext extends Context> = Record<PropertyKey, ContextAttribute<TContext, StateValue<any>>>;
10
- export type ContextResolver<T> = (context: Context) => StateValue<T>;
11
- export type StateValue<T> = T;
12
- export declare enum DependencyType {
13
- VALUE = "VALUE",
14
- FACTORY = "FACTORY",
15
- CLASS = "CLASS"
16
- }
17
- export type FactoryFn<T, A extends any[]> = (...args: A) => T;
18
- export type ValueLoader<C extends Context, T> = (context: C) => T;
19
- export type FilterKeysByType<TScope, TValue> = {
20
- [Key in keyof TScope]: TScope[Key] extends TValue ? Key : never;
21
- }[keyof TScope];
22
- export type FilterKeysByAsyncType<TScope, TValue> = {
23
- [Key in keyof TScope]: TScope[Key] extends PromiseOrValue<TValue> ? Key : never;
24
- }[keyof TScope];
25
- export type FilterArgsByType<TScope, TArgs extends any[]> = {
26
- [Key in keyof TArgs]: FilterKeysByType<TScope, TArgs[Key]>;
27
- };
28
- export type FilterAsyncArgsByType<TScope, TArgs extends any[]> = {
29
- [Key in keyof TArgs]: FilterKeysByAsyncType<TScope, TArgs[Key]>;
30
- };
31
- export type ContextAttribute<TContext extends Context<any>, TValue> = {
32
- key: PropertyKey;
33
- value?: ValueLoader<TContext, TValue>;
34
- type: DependencyType;
35
- resolveOptions: ResolveOptions;
36
- cachedValue?: TValue;
37
- inject: PropertyKey[];
38
- };
39
- export type ScopeKey<S extends object> = keyof S;
40
- export type ContextListener = (context: Context) => any;
41
- export type ResolveOptions = {
42
- strict?: boolean;
43
- eager?: boolean;
44
- singleton?: boolean;
45
- };
46
- export declare function defaultResolveOptions(): ResolveOptions;
47
- export type ContextOptions = {
48
- defaultResolveOptions?: ResolveOptions;
49
- };
50
- export declare class Context<Scope extends object = any> {
51
- private readonly state;
52
- private readonly emitter;
53
- readonly scope: Scope;
54
- protected readonly defaultResolveOptions: ResolveOptions;
55
- constructor(options?: ContextOptions);
56
- /**
57
- * Set a value in context, to be injected later.
58
- *
59
- * @param key
60
- * @param payload
61
- * @deprecated Use `provideValue` instead, or you can register the same dependency as a factory with `provideFactory` or class with `provideClass`.
62
- */
63
- provide<K extends ScopeKey<Scope>>(key: K, payload: Scope[K]): void;
64
- /**
65
- * Manually register a dependency. This should normally be used by utils or integrations that need to register dependencies in creative ways. For normal use cases, using `provideValue`, `provideFactory`, or `provideClass` is sufficient.
66
- *
67
- * @param key The key to register the dependency under
68
- * @param dep The dependency record
69
- */
70
- register<K extends ScopeKey<Scope>>(key: K, dep: ContextAttribute<this, Scope[K]>): void;
71
- /**
72
- * Register a value in context scope.
73
- *
74
- * @param key The key to register the dependency under
75
- * @param value The value to register
76
- */
77
- registerValue<K extends ScopeKey<Scope>>(key: K, value: Scope[K], defaultResolveOptions?: ResolveOptions): this;
78
- /**
79
- * Register a dependency as a factory in context scope.
80
- *
81
- * @param key The key to register the dependency under
82
- * @param factory A factory function that will be called to generate the value when it is requested.
83
- * @param inject An array of keys to use when injecting factory args.
84
- * @returns A chainable instance of context
85
- */
86
- registerFactory<K extends ScopeKey<Scope>, A extends any[]>(key: K, factory: FactoryFn<Scope[K], A>, inject?: FilterArgsByType<Scope, A>, defaultResolveOptions?: ResolveOptions): this;
87
- registerAsyncFactory<K extends FilterKeysByType<Scope, Promise<any>>, A extends any[]>(key: K, factory: FactoryFn<Scope[K], A>, inject?: FilterAsyncArgsByType<Scope, A>, defaultResolveOptions?: ResolveOptions): this;
88
- registerClass<K extends FilterKeysByType<Scope, InstanceType<T>>, T extends Class<Scope[K]>>(key: K, Class: T, inject?: FilterArgsByType<Scope, ConstructorParameters<T>>, defaultResolveOptions?: ResolveOptions): this;
89
- registerAsyncClass<K extends FilterKeysByType<Scope, InstanceType<any>>, T extends Class<UnwrapPromise<Scope[K]>>>(key: K, Class: T, inject?: FilterAsyncArgsByType<Scope, ConstructorParameters<T>>, defaultResolveOptions?: ResolveOptions): this;
90
- getAllDependencies<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[];
91
- getAllDependents<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[];
92
- introspect<K extends ScopeKey<Scope>>(key: K): ContextState<this>[K];
93
- protected _cacheIsValid<K extends ScopeKey<Scope>>(key: K): boolean;
94
- protected _resolveValue<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K];
95
- /**
96
- * Extract a value from context.
97
- *
98
- * @param key
99
- * @returns
100
- * @deprecated Use `resolve` instead
101
- */
102
- inject<T = any>(key: ScopeKey<Scope>): T | undefined;
103
- resolve<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K];
104
- /**
105
- * Ensure that only one copy of this instance exists in this context. Provides the instance if it doesn't exist yet, otherwise inject the cached instance.
106
- *
107
- * @param key
108
- * @param instance
109
- */
110
- singleton<T = any>(key: ScopeKey<Scope>, instance: T): T;
111
- /**
112
- * Instantiate a ContextConsumer class
113
- *
114
- * @param Consumer
115
- * @returns
116
- */
117
- hydrate<T extends ContextConsumer<this, any[]>, A extends any[]>(Consumer: new (context: this, ...args: A) => T, ...args: A): T;
118
- /**
119
- * Create a new context from other instance(s) of Context
120
- *
121
- * @param contexts
122
- * @returns
123
- */
124
- extend(...contexts: Context[]): this;
125
- /**
126
- * Modify context with middleware
127
- *
128
- * @param middleware
129
- * @returns
130
- */
131
- use(...middleware: ContextMiddleware[]): Promise<any>;
132
- /**
133
- * Validate context ensuring all validators are valid.
134
- *
135
- * @param validators
136
- * @returns
137
- */
138
- validate(...validators: Validator<Context>[]): import('..').Validation<Context<any>>;
139
- /**
140
- * Validate context ensuring at least one validator is valid
141
- * @param validators
142
- * @returns
143
- */
144
- validateAny(...validators: Validator<Context>[]): import('..').Validation<any>;
145
- /**
146
- * Add a callback to listen for an event in this context.
147
- *
148
- * @param event
149
- * @param listener
150
- * @returns
151
- */
152
- on(event: string, listener: ContextListener): () => void;
153
- /**
154
- * Emit an event in this context
155
- *
156
- * @param event
157
- * @param args
158
- * @returns
159
- */
160
- emit(event: string): Promise<void>;
161
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};