@effect-gql/core 0.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.
Files changed (145) hide show
  1. package/LICENSE +7 -0
  2. package/dist/analyzer-extension.d.ts +105 -0
  3. package/dist/analyzer-extension.d.ts.map +1 -0
  4. package/dist/analyzer-extension.js +137 -0
  5. package/dist/analyzer-extension.js.map +1 -0
  6. package/dist/builder/execute.d.ts +26 -0
  7. package/dist/builder/execute.d.ts.map +1 -0
  8. package/dist/builder/execute.js +104 -0
  9. package/dist/builder/execute.js.map +1 -0
  10. package/dist/builder/field-builders.d.ts +30 -0
  11. package/dist/builder/field-builders.d.ts.map +1 -0
  12. package/dist/builder/field-builders.js +200 -0
  13. package/dist/builder/field-builders.js.map +1 -0
  14. package/dist/builder/index.d.ts +7 -0
  15. package/dist/builder/index.d.ts.map +1 -0
  16. package/dist/builder/index.js +31 -0
  17. package/dist/builder/index.js.map +1 -0
  18. package/dist/builder/pipe-api.d.ts +231 -0
  19. package/dist/builder/pipe-api.d.ts.map +1 -0
  20. package/dist/builder/pipe-api.js +151 -0
  21. package/dist/builder/pipe-api.js.map +1 -0
  22. package/dist/builder/schema-builder.d.ts +301 -0
  23. package/dist/builder/schema-builder.d.ts.map +1 -0
  24. package/dist/builder/schema-builder.js +566 -0
  25. package/dist/builder/schema-builder.js.map +1 -0
  26. package/dist/builder/type-registry.d.ts +80 -0
  27. package/dist/builder/type-registry.d.ts.map +1 -0
  28. package/dist/builder/type-registry.js +505 -0
  29. package/dist/builder/type-registry.js.map +1 -0
  30. package/dist/builder/types.d.ts +283 -0
  31. package/dist/builder/types.d.ts.map +1 -0
  32. package/dist/builder/types.js +3 -0
  33. package/dist/builder/types.js.map +1 -0
  34. package/dist/cli/generate-schema.d.ts +29 -0
  35. package/dist/cli/generate-schema.d.ts.map +1 -0
  36. package/dist/cli/generate-schema.js +233 -0
  37. package/dist/cli/generate-schema.js.map +1 -0
  38. package/dist/cli/index.d.ts +19 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +24 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/context.d.ts +18 -0
  43. package/dist/context.d.ts.map +1 -0
  44. package/dist/context.js +11 -0
  45. package/dist/context.js.map +1 -0
  46. package/dist/error.d.ts +45 -0
  47. package/dist/error.d.ts.map +1 -0
  48. package/dist/error.js +29 -0
  49. package/dist/error.js.map +1 -0
  50. package/dist/extensions.d.ts +130 -0
  51. package/dist/extensions.d.ts.map +1 -0
  52. package/dist/extensions.js +78 -0
  53. package/dist/extensions.js.map +1 -0
  54. package/dist/index.d.ts +12 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +47 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/loader.d.ts +169 -0
  59. package/dist/loader.d.ts.map +1 -0
  60. package/dist/loader.js +237 -0
  61. package/dist/loader.js.map +1 -0
  62. package/dist/resolver-context.d.ts +154 -0
  63. package/dist/resolver-context.d.ts.map +1 -0
  64. package/dist/resolver-context.js +184 -0
  65. package/dist/resolver-context.js.map +1 -0
  66. package/dist/schema-mapping.d.ts +30 -0
  67. package/dist/schema-mapping.d.ts.map +1 -0
  68. package/dist/schema-mapping.js +280 -0
  69. package/dist/schema-mapping.js.map +1 -0
  70. package/dist/server/cache-control.d.ts +96 -0
  71. package/dist/server/cache-control.d.ts.map +1 -0
  72. package/dist/server/cache-control.js +308 -0
  73. package/dist/server/cache-control.js.map +1 -0
  74. package/dist/server/complexity.d.ts +165 -0
  75. package/dist/server/complexity.d.ts.map +1 -0
  76. package/dist/server/complexity.js +433 -0
  77. package/dist/server/complexity.js.map +1 -0
  78. package/dist/server/config.d.ts +66 -0
  79. package/dist/server/config.d.ts.map +1 -0
  80. package/dist/server/config.js +104 -0
  81. package/dist/server/config.js.map +1 -0
  82. package/dist/server/graphiql.d.ts +5 -0
  83. package/dist/server/graphiql.d.ts.map +1 -0
  84. package/dist/server/graphiql.js +43 -0
  85. package/dist/server/graphiql.js.map +1 -0
  86. package/dist/server/index.d.ts +18 -0
  87. package/dist/server/index.d.ts.map +1 -0
  88. package/dist/server/index.js +48 -0
  89. package/dist/server/index.js.map +1 -0
  90. package/dist/server/router.d.ts +79 -0
  91. package/dist/server/router.d.ts.map +1 -0
  92. package/dist/server/router.js +232 -0
  93. package/dist/server/router.js.map +1 -0
  94. package/dist/server/schema-builder-extensions.d.ts +42 -0
  95. package/dist/server/schema-builder-extensions.d.ts.map +1 -0
  96. package/dist/server/schema-builder-extensions.js +48 -0
  97. package/dist/server/schema-builder-extensions.js.map +1 -0
  98. package/dist/server/sse-adapter.d.ts +64 -0
  99. package/dist/server/sse-adapter.d.ts.map +1 -0
  100. package/dist/server/sse-adapter.js +227 -0
  101. package/dist/server/sse-adapter.js.map +1 -0
  102. package/dist/server/sse-types.d.ts +192 -0
  103. package/dist/server/sse-types.d.ts.map +1 -0
  104. package/dist/server/sse-types.js +63 -0
  105. package/dist/server/sse-types.js.map +1 -0
  106. package/dist/server/ws-adapter.d.ts +39 -0
  107. package/dist/server/ws-adapter.d.ts.map +1 -0
  108. package/dist/server/ws-adapter.js +247 -0
  109. package/dist/server/ws-adapter.js.map +1 -0
  110. package/dist/server/ws-types.d.ts +169 -0
  111. package/dist/server/ws-types.d.ts.map +1 -0
  112. package/dist/server/ws-types.js +11 -0
  113. package/dist/server/ws-types.js.map +1 -0
  114. package/dist/server/ws-utils.d.ts +42 -0
  115. package/dist/server/ws-utils.d.ts.map +1 -0
  116. package/dist/server/ws-utils.js +99 -0
  117. package/dist/server/ws-utils.js.map +1 -0
  118. package/package.json +61 -0
  119. package/src/analyzer-extension.ts +254 -0
  120. package/src/builder/execute.ts +153 -0
  121. package/src/builder/field-builders.ts +322 -0
  122. package/src/builder/index.ts +48 -0
  123. package/src/builder/pipe-api.ts +312 -0
  124. package/src/builder/schema-builder.ts +970 -0
  125. package/src/builder/type-registry.ts +670 -0
  126. package/src/builder/types.ts +305 -0
  127. package/src/context.ts +23 -0
  128. package/src/error.ts +32 -0
  129. package/src/extensions.ts +240 -0
  130. package/src/index.ts +32 -0
  131. package/src/loader.ts +363 -0
  132. package/src/resolver-context.ts +253 -0
  133. package/src/schema-mapping.ts +307 -0
  134. package/src/server/cache-control.ts +590 -0
  135. package/src/server/complexity.ts +774 -0
  136. package/src/server/config.ts +174 -0
  137. package/src/server/graphiql.ts +38 -0
  138. package/src/server/index.ts +96 -0
  139. package/src/server/router.ts +432 -0
  140. package/src/server/schema-builder-extensions.ts +51 -0
  141. package/src/server/sse-adapter.ts +327 -0
  142. package/src/server/sse-types.ts +234 -0
  143. package/src/server/ws-adapter.ts +355 -0
  144. package/src/server/ws-types.ts +192 -0
  145. package/src/server/ws-utils.ts +136 -0
package/src/loader.ts ADDED
@@ -0,0 +1,363 @@
1
+ import { Effect, Context, Layer } from "effect"
2
+ import DataLoader from "dataloader"
3
+
4
+ /**
5
+ * Ergonomic DataLoader helpers for Effect-based GraphQL
6
+ *
7
+ * This module provides a type-safe, declarative way to define DataLoaders
8
+ * that integrate seamlessly with Effect's service system.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Define loaders
13
+ * const loaders = Loader.define({
14
+ * UserById: Loader.single<string, User>({
15
+ * batch: (ids) => db.getUsersByIds(ids),
16
+ * key: (user) => user.id,
17
+ * }),
18
+ *
19
+ * PostsByAuthorId: Loader.grouped<string, Post>({
20
+ * batch: (ids) => db.getPostsForAuthors(ids),
21
+ * groupBy: (post) => post.authorId,
22
+ * }),
23
+ * })
24
+ *
25
+ * // Use in resolvers
26
+ * resolve: (parent) => loaders.load("UserById", parent.authorId)
27
+ * ```
28
+ */
29
+
30
+ // ============================================================================
31
+ // Types
32
+ // ============================================================================
33
+
34
+ /**
35
+ * Configuration for a single-value loader (one key -> one value)
36
+ */
37
+ interface SingleLoaderDef<K, V, R> {
38
+ readonly _tag: "single"
39
+ readonly batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
40
+ readonly key: (value: V) => K
41
+ }
42
+
43
+ /**
44
+ * Configuration for a grouped loader (one key -> many values)
45
+ */
46
+ interface GroupedLoaderDef<K, V, R> {
47
+ readonly _tag: "grouped"
48
+ readonly batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
49
+ readonly groupBy: (value: V) => K
50
+ }
51
+
52
+ type LoaderDef<K, V, R> = SingleLoaderDef<K, V, R> | GroupedLoaderDef<K, V, R>
53
+
54
+ /**
55
+ * Runtime DataLoader instances
56
+ */
57
+ type LoaderInstances<Defs extends Record<string, LoaderDef<any, any, any>>> = {
58
+ [Name in keyof Defs]: Defs[Name] extends SingleLoaderDef<infer K, infer V, any>
59
+ ? DataLoader<K, V>
60
+ : Defs[Name] extends GroupedLoaderDef<infer K, infer V, any>
61
+ ? DataLoader<K, V[]>
62
+ : never
63
+ }
64
+
65
+ /**
66
+ * Extract the value type for a loader (accounting for grouped loaders)
67
+ */
68
+ type LoaderValue<Def> =
69
+ Def extends SingleLoaderDef<any, infer V, any>
70
+ ? V
71
+ : Def extends GroupedLoaderDef<any, infer V, any>
72
+ ? V[]
73
+ : never
74
+
75
+ /**
76
+ * Extract the key type for a loader
77
+ */
78
+ type LoaderKey<Def> = Def extends LoaderDef<infer K, any, any> ? K : never
79
+
80
+ /**
81
+ * Extract combined requirements from all loaders
82
+ */
83
+ type LoaderRequirements<Defs extends Record<string, LoaderDef<any, any, any>>> = {
84
+ [K in keyof Defs]: Defs[K] extends LoaderDef<any, any, infer R> ? R : never
85
+ }[keyof Defs]
86
+
87
+ // ============================================================================
88
+ // Loader Builders
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Create a single-value loader definition.
93
+ * One key maps to one value.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * Loader.single<string, User>({
98
+ * batch: (ids) => db.getUsersByIds(ids),
99
+ * key: (user) => user.id,
100
+ * })
101
+ * ```
102
+ */
103
+ function single<K, V, R = never>(config: {
104
+ batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
105
+ key: (value: V) => K
106
+ }): SingleLoaderDef<K, V, R> {
107
+ return {
108
+ _tag: "single",
109
+ batch: config.batch,
110
+ key: config.key,
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Create a grouped loader definition.
116
+ * One key maps to many values.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * Loader.grouped<string, Post>({
121
+ * batch: (authorIds) => db.getPostsForAuthors(authorIds),
122
+ * groupBy: (post) => post.authorId,
123
+ * })
124
+ * ```
125
+ */
126
+ function grouped<K, V, R = never>(config: {
127
+ batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
128
+ groupBy: (value: V) => K
129
+ }): GroupedLoaderDef<K, V, R> {
130
+ return {
131
+ _tag: "grouped",
132
+ batch: config.batch,
133
+ groupBy: config.groupBy,
134
+ }
135
+ }
136
+
137
+ // ============================================================================
138
+ // Loader Registry
139
+ // ============================================================================
140
+
141
+ /**
142
+ * A registry of loader definitions with methods to create instances and layers
143
+ */
144
+ class LoaderRegistry<Defs extends Record<string, LoaderDef<any, any, any>>> {
145
+ readonly _tag = "LoaderRegistry"
146
+
147
+ /**
148
+ * The Effect service tag for this loader registry
149
+ */
150
+ readonly Service: Context.Tag<LoaderInstances<Defs>, LoaderInstances<Defs>>
151
+
152
+ constructor(readonly definitions: Defs) {
153
+ this.Service = Context.GenericTag<LoaderInstances<Defs>>(
154
+ `DataLoaders(${Object.keys(definitions).join(", ")})`
155
+ )
156
+ }
157
+
158
+ /**
159
+ * Create a Layer that provides fresh DataLoader instances.
160
+ * Call this once per request to get request-scoped loaders.
161
+ */
162
+ toLayer(): Layer.Layer<LoaderInstances<Defs>, never, LoaderRequirements<Defs>> {
163
+ const self = this
164
+ return Layer.effect(
165
+ this.Service,
166
+ Effect.gen(function* () {
167
+ const instances: Record<string, DataLoader<any, any>> = {}
168
+
169
+ for (const [name, def] of Object.entries(self.definitions)) {
170
+ instances[name] = yield* createDataLoader(def)
171
+ }
172
+
173
+ return instances as LoaderInstances<Defs>
174
+ })
175
+ ) as Layer.Layer<LoaderInstances<Defs>, never, LoaderRequirements<Defs>>
176
+ }
177
+
178
+ /**
179
+ * Helper to use loaders in a resolver with a callback.
180
+ */
181
+ use<A>(
182
+ fn: (loaders: LoaderInstances<Defs>) => Promise<A>
183
+ ): Effect.Effect<A, Error, LoaderInstances<Defs>> {
184
+ const self = this
185
+ return Effect.gen(function* () {
186
+ const loaders = yield* self.Service
187
+ return yield* Effect.promise(() => fn(loaders))
188
+ })
189
+ }
190
+
191
+ /**
192
+ * Load a single value by key.
193
+ * This is the most common operation in resolvers.
194
+ */
195
+ load<Name extends keyof Defs & string>(
196
+ name: Name,
197
+ key: LoaderKey<Defs[Name]>
198
+ ): Effect.Effect<LoaderValue<Defs[Name]>, Error, LoaderInstances<Defs>> {
199
+ const self = this
200
+ return Effect.gen(function* () {
201
+ const loaders = yield* self.Service
202
+ const loader = loaders[name] as DataLoader<any, any>
203
+ return yield* Effect.promise(() => loader.load(key))
204
+ })
205
+ }
206
+
207
+ /**
208
+ * Load multiple values by keys.
209
+ * All keys are batched into a single request.
210
+ */
211
+ loadMany<Name extends keyof Defs & string>(
212
+ name: Name,
213
+ keys: readonly LoaderKey<Defs[Name]>[]
214
+ ): Effect.Effect<readonly LoaderValue<Defs[Name]>[], Error, LoaderInstances<Defs>> {
215
+ const self = this
216
+ return Effect.gen(function* () {
217
+ const loaders = yield* self.Service
218
+ const loader = loaders[name] as DataLoader<any, any>
219
+ const results = yield* Effect.promise(() => loader.loadMany(keys))
220
+ // Convert any errors to a failure
221
+ for (const result of results) {
222
+ if (result instanceof Error) {
223
+ return yield* Effect.fail(result)
224
+ }
225
+ }
226
+ return results as readonly LoaderValue<Defs[Name]>[]
227
+ })
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Create a DataLoader from a loader definition
233
+ */
234
+ function createDataLoader<K, V, R>(
235
+ def: LoaderDef<K, V, R>
236
+ ): Effect.Effect<DataLoader<K, any>, never, R> {
237
+ return Effect.gen(function* () {
238
+ // Capture context for use in batch function
239
+ const context = yield* Effect.context<R>()
240
+
241
+ if (def._tag === "single") {
242
+ const loader = new DataLoader<K, V>(async (keys) => {
243
+ const items = await Effect.runPromise(def.batch(keys).pipe(Effect.provide(context)))
244
+ // Map items back to keys in order
245
+ return keys.map((key) => {
246
+ const item = items.find((i) => def.key(i) === key)
247
+ if (!item) return new Error(`Not found: ${key}`) as any
248
+ return item
249
+ })
250
+ })
251
+ return loader
252
+ } else {
253
+ // Grouped loader
254
+ const loader = new DataLoader<K, V[]>(async (keys) => {
255
+ const items = await Effect.runPromise(def.batch(keys).pipe(Effect.provide(context)))
256
+ // Group items by key with lazy array initialization
257
+ // Only create arrays for keys that have matching items
258
+ const map = new Map<K, V[]>()
259
+ for (const item of items) {
260
+ const key = def.groupBy(item)
261
+ let arr = map.get(key)
262
+ if (!arr) {
263
+ arr = []
264
+ map.set(key, arr)
265
+ }
266
+ arr.push(item)
267
+ }
268
+ // Return results in key order, defaulting to empty array for missing keys
269
+ return keys.map((key) => map.get(key) ?? [])
270
+ })
271
+ return loader
272
+ }
273
+ })
274
+ }
275
+
276
+ // ============================================================================
277
+ // Public API
278
+ // ============================================================================
279
+
280
+ /**
281
+ * Define a set of loaders.
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const loaders = Loader.define({
286
+ * UserById: Loader.single<string, User>({
287
+ * batch: (ids) => db.getUsersByIds(ids),
288
+ * key: (user) => user.id,
289
+ * }),
290
+ * PostsByAuthorId: Loader.grouped<string, Post>({
291
+ * batch: (ids) => db.getPostsForAuthors(ids),
292
+ * groupBy: (post) => post.authorId,
293
+ * }),
294
+ * })
295
+ *
296
+ * // In resolvers:
297
+ * loaders.load("UserById", "123")
298
+ * loaders.loadMany("UserById", ["1", "2", "3"])
299
+ * ```
300
+ */
301
+ function define<Defs extends Record<string, LoaderDef<any, any, any>>>(
302
+ definitions: Defs
303
+ ): LoaderRegistry<Defs> {
304
+ return new LoaderRegistry(definitions)
305
+ }
306
+
307
+ // ============================================================================
308
+ // Utility Functions
309
+ // ============================================================================
310
+
311
+ /**
312
+ * Map an array of items to match requested keys.
313
+ * Returns items in the same order as keys, with errors for missing items.
314
+ */
315
+ function mapByKey<K, V>(
316
+ keys: readonly K[],
317
+ items: readonly V[],
318
+ keyFn: (item: V) => K
319
+ ): (V | Error)[] {
320
+ const map = new Map<K, V>()
321
+ for (const item of items) {
322
+ map.set(keyFn(item), item)
323
+ }
324
+ return keys.map((key) => map.get(key) ?? new Error(`Not found: ${key}`))
325
+ }
326
+
327
+ /**
328
+ * Group an array of items by a key function.
329
+ * Returns a Map from key to array of matching items.
330
+ * Guarantees an entry (possibly empty) for each requested key.
331
+ */
332
+ function groupByKey<K, V>(
333
+ keys: readonly K[],
334
+ items: readonly V[],
335
+ keyFn: (item: V) => K
336
+ ): Map<K, V[]> {
337
+ const map = new Map<K, V[]>()
338
+ // Initialize empty arrays for all requested keys
339
+ for (const key of keys) {
340
+ map.set(key, [])
341
+ }
342
+ // Fill in items
343
+ for (const item of items) {
344
+ const key = keyFn(item)
345
+ const arr = map.get(key)
346
+ if (arr) arr.push(item)
347
+ }
348
+ return map
349
+ }
350
+
351
+ // ============================================================================
352
+ // Export
353
+ // ============================================================================
354
+
355
+ export const Loader = {
356
+ define,
357
+ single,
358
+ grouped,
359
+ mapByKey,
360
+ groupByKey,
361
+ } as const
362
+
363
+ export type { LoaderRegistry, LoaderDef, LoaderInstances }
@@ -0,0 +1,253 @@
1
+ import { Effect, Context, Layer, Ref, Option, HashMap } from "effect"
2
+
3
+ /**
4
+ * A type-safe context system for passing values through the resolver hierarchy.
5
+ *
6
+ * Unlike simple property bags, this provides:
7
+ * - Type-safe slots that know their value type
8
+ * - Clear errors when required context is missing
9
+ * - Request-scoped storage that works across nested resolvers
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // Define a context slot
14
+ * const AuthPrincipal = ResolverContext.make<User>("AuthPrincipal")
15
+ *
16
+ * // Provide in a directive
17
+ * .directive({
18
+ * name: "auth",
19
+ * apply: () => (effect) => Effect.gen(function*() {
20
+ * const user = yield* validateJwt()
21
+ * yield* ResolverContext.set(AuthPrincipal, user)
22
+ * return yield* effect
23
+ * }),
24
+ * })
25
+ *
26
+ * // Access in any nested resolver
27
+ * .field("User", "posts", {
28
+ * resolve: (parent) => Effect.gen(function*() {
29
+ * const user = yield* ResolverContext.get(AuthPrincipal)
30
+ * // ...
31
+ * }),
32
+ * })
33
+ * ```
34
+ */
35
+
36
+ /**
37
+ * Error thrown when trying to access a context value that hasn't been set
38
+ */
39
+ export class MissingResolverContextError extends Error {
40
+ readonly _tag = "MissingResolverContextError"
41
+
42
+ constructor(readonly contextName: string) {
43
+ super(
44
+ `Resolver context "${contextName}" was not provided. Ensure a parent resolver or directive provides this context.`
45
+ )
46
+ this.name = "MissingResolverContextError"
47
+ }
48
+ }
49
+
50
+ /**
51
+ * A typed context slot that can hold a value of type A
52
+ */
53
+ export interface ResolverContextSlot<A> {
54
+ readonly _tag: "ResolverContextSlot"
55
+ readonly name: string
56
+ readonly _A: A // Phantom type for type inference
57
+ }
58
+
59
+ /**
60
+ * Internal storage for resolver context values.
61
+ * This is a request-scoped service that holds all context values.
62
+ */
63
+ export interface ResolverContextStore {
64
+ readonly ref: Ref.Ref<HashMap.HashMap<string, unknown>>
65
+ }
66
+
67
+ export const ResolverContextStore = Context.GenericTag<ResolverContextStore>(
68
+ "effect-gql/ResolverContextStore"
69
+ )
70
+
71
+ /**
72
+ * Create a Layer that provides the ResolverContextStore.
73
+ * This should be included in the request layer.
74
+ */
75
+ export const makeStoreLayer = (): Effect.Effect<Layer.Layer<ResolverContextStore>> =>
76
+ Effect.map(Ref.make(HashMap.empty<string, unknown>()), (ref) =>
77
+ Layer.succeed(ResolverContextStore, { ref })
78
+ )
79
+
80
+ /**
81
+ * Create a Layer that provides an empty ResolverContextStore.
82
+ * Convenience function for creating a fresh store layer.
83
+ */
84
+ export const storeLayer: Layer.Layer<ResolverContextStore> = Layer.effect(
85
+ ResolverContextStore,
86
+ Effect.map(Ref.make(HashMap.empty<string, unknown>()), (ref) => ({ ref }))
87
+ )
88
+
89
+ /**
90
+ * Create a new resolver context slot.
91
+ *
92
+ * The name is used for error messages when the context is accessed but not set.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const AuthPrincipal = ResolverContext.make<User>("AuthPrincipal")
97
+ * const TenantId = ResolverContext.make<string>("TenantId")
98
+ * ```
99
+ */
100
+ export const make = <A>(name: string): ResolverContextSlot<A> => ({
101
+ _tag: "ResolverContextSlot",
102
+ name,
103
+ _A: undefined as unknown as A,
104
+ })
105
+
106
+ /**
107
+ * Get a value from the resolver context.
108
+ *
109
+ * Fails with MissingResolverContextError if the context was not set
110
+ * by a parent resolver or directive.
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const effect = Effect.gen(function*() {
115
+ * const user = yield* ResolverContext.get(AuthPrincipal)
116
+ * // user is typed as User
117
+ * })
118
+ * ```
119
+ */
120
+ export const get = <A>(
121
+ slot: ResolverContextSlot<A>
122
+ ): Effect.Effect<A, MissingResolverContextError, ResolverContextStore> =>
123
+ Effect.flatMap(ResolverContextStore, (store) =>
124
+ Effect.flatMap(Ref.get(store.ref), (map) => {
125
+ const value = HashMap.get(map, slot.name)
126
+ return Option.match(value, {
127
+ onNone: () => Effect.fail(new MissingResolverContextError(slot.name)),
128
+ onSome: (v) => Effect.succeed(v as A),
129
+ })
130
+ })
131
+ )
132
+
133
+ /**
134
+ * Get a value from the resolver context as an Option.
135
+ *
136
+ * Returns None if the context was not set, instead of failing.
137
+ * Useful when context is optional.
138
+ */
139
+ export const getOption = <A>(
140
+ slot: ResolverContextSlot<A>
141
+ ): Effect.Effect<Option.Option<A>, never, ResolverContextStore> =>
142
+ Effect.flatMap(ResolverContextStore, (store) =>
143
+ Effect.map(Ref.get(store.ref), (map) => HashMap.get(map, slot.name) as Option.Option<A>)
144
+ )
145
+
146
+ /**
147
+ * Set a value in the resolver context.
148
+ *
149
+ * The value will be available to all subsequent resolver calls in this request.
150
+ * This mutates the request-scoped store, so nested resolvers will see the value.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * // In a directive
155
+ * const withAuth = (effect) => Effect.gen(function*() {
156
+ * const user = yield* validateJwt()
157
+ * yield* ResolverContext.set(AuthPrincipal, user)
158
+ * return yield* effect
159
+ * })
160
+ * ```
161
+ */
162
+ export const set = <A>(
163
+ slot: ResolverContextSlot<A>,
164
+ value: A
165
+ ): Effect.Effect<void, never, ResolverContextStore> =>
166
+ Effect.flatMap(ResolverContextStore, (store) =>
167
+ Ref.update(store.ref, (map) => HashMap.set(map, slot.name, value))
168
+ )
169
+
170
+ /**
171
+ * Set multiple context values at once.
172
+ */
173
+ export const setMany = (
174
+ values: ReadonlyArray<readonly [ResolverContextSlot<any>, any]>
175
+ ): Effect.Effect<void, never, ResolverContextStore> =>
176
+ Effect.flatMap(ResolverContextStore, (store) =>
177
+ Ref.update(store.ref, (map) => {
178
+ let result = map
179
+ for (const [slot, value] of values) {
180
+ result = HashMap.set(result, slot.name, value)
181
+ }
182
+ return result
183
+ })
184
+ )
185
+
186
+ /**
187
+ * Check if a context slot has a value set.
188
+ */
189
+ export const has = <A>(
190
+ slot: ResolverContextSlot<A>
191
+ ): Effect.Effect<boolean, never, ResolverContextStore> =>
192
+ Effect.flatMap(ResolverContextStore, (store) =>
193
+ Effect.map(Ref.get(store.ref), (map) => HashMap.has(map, slot.name))
194
+ )
195
+
196
+ /**
197
+ * Get a value or return a default if not set.
198
+ */
199
+ export const getOrElse = <A>(
200
+ slot: ResolverContextSlot<A>,
201
+ orElse: () => A
202
+ ): Effect.Effect<A, never, ResolverContextStore> =>
203
+ Effect.flatMap(ResolverContextStore, (store) =>
204
+ Effect.map(Ref.get(store.ref), (map) =>
205
+ Option.getOrElse(HashMap.get(map, slot.name) as Option.Option<A>, orElse)
206
+ )
207
+ )
208
+
209
+ /**
210
+ * Run an effect with a temporary context value.
211
+ * The value is set before the effect runs and removed after.
212
+ * Useful for scoped context that shouldn't persist.
213
+ */
214
+ export const scoped =
215
+ <A>(slot: ResolverContextSlot<A>, value: A) =>
216
+ <B, E, R>(effect: Effect.Effect<B, E, R>): Effect.Effect<B, E, R | ResolverContextStore> =>
217
+ Effect.flatMap(ResolverContextStore, (store) =>
218
+ Effect.acquireUseRelease(
219
+ // Acquire: save current value and set new one
220
+ Effect.flatMap(Ref.get(store.ref), (map) => {
221
+ const previous = HashMap.get(map, slot.name)
222
+ return Effect.as(Ref.set(store.ref, HashMap.set(map, slot.name, value)), previous)
223
+ }),
224
+ // Use: run the effect
225
+ () => effect,
226
+ // Release: restore previous value
227
+ (previous) =>
228
+ Ref.update(store.ref, (map) =>
229
+ Option.match(previous, {
230
+ onNone: () => HashMap.remove(map, slot.name),
231
+ onSome: (v) => HashMap.set(map, slot.name, v),
232
+ })
233
+ )
234
+ )
235
+ )
236
+
237
+ /**
238
+ * Namespace for ResolverContext functions
239
+ */
240
+ export const ResolverContext = {
241
+ make,
242
+ get,
243
+ getOption,
244
+ set,
245
+ setMany,
246
+ has,
247
+ getOrElse,
248
+ scoped,
249
+ storeLayer,
250
+ makeStoreLayer,
251
+ Store: ResolverContextStore,
252
+ MissingResolverContextError,
253
+ } as const