@effect-app/infra 4.0.0-beta.12 → 4.0.0-beta.121

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 (176) hide show
  1. package/CHANGELOG.md +802 -0
  2. package/dist/CUPS.d.ts +3 -3
  3. package/dist/CUPS.d.ts.map +1 -1
  4. package/dist/CUPS.js +3 -3
  5. package/dist/Emailer/service.d.ts +3 -3
  6. package/dist/Emailer/service.d.ts.map +1 -1
  7. package/dist/Emailer/service.js +3 -3
  8. package/dist/MainFiberSet.d.ts +2 -2
  9. package/dist/MainFiberSet.d.ts.map +1 -1
  10. package/dist/MainFiberSet.js +3 -3
  11. package/dist/Model/Repository/Registry.d.ts +20 -0
  12. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  13. package/dist/Model/Repository/Registry.js +17 -0
  14. package/dist/Model/Repository/internal/internal.d.ts +3 -3
  15. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  16. package/dist/Model/Repository/internal/internal.js +22 -16
  17. package/dist/Model/Repository/makeRepo.d.ts +5 -4
  18. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  19. package/dist/Model/Repository/makeRepo.js +4 -1
  20. package/dist/Model/Repository/service.d.ts +5 -0
  21. package/dist/Model/Repository/service.d.ts.map +1 -1
  22. package/dist/Model/Repository/validation.d.ts +7 -6
  23. package/dist/Model/Repository/validation.d.ts.map +1 -1
  24. package/dist/Model/Repository.d.ts +1 -0
  25. package/dist/Model/Repository.d.ts.map +1 -1
  26. package/dist/Model/Repository.js +2 -1
  27. package/dist/Model/query/dsl.d.ts +9 -9
  28. package/dist/Model/query/new-kid-interpreter.d.ts +2 -2
  29. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  30. package/dist/Model/query/new-kid-interpreter.js +3 -3
  31. package/dist/Model.d.ts +1 -0
  32. package/dist/Model.d.ts.map +1 -1
  33. package/dist/Model.js +2 -1
  34. package/dist/Operations.d.ts +2 -2
  35. package/dist/Operations.d.ts.map +1 -1
  36. package/dist/Operations.js +3 -3
  37. package/dist/OperationsRepo.d.ts +3 -3
  38. package/dist/OperationsRepo.d.ts.map +1 -1
  39. package/dist/OperationsRepo.js +3 -3
  40. package/dist/QueueMaker/SQLQueue.d.ts +2 -4
  41. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  42. package/dist/QueueMaker/SQLQueue.js +8 -6
  43. package/dist/QueueMaker/errors.d.ts +1 -1
  44. package/dist/QueueMaker/errors.d.ts.map +1 -1
  45. package/dist/QueueMaker/memQueue.js +3 -3
  46. package/dist/QueueMaker/sbqueue.js +3 -3
  47. package/dist/RequestContext.d.ts +22 -17
  48. package/dist/RequestContext.d.ts.map +1 -1
  49. package/dist/RequestContext.js +5 -5
  50. package/dist/RequestFiberSet.d.ts +2 -2
  51. package/dist/RequestFiberSet.d.ts.map +1 -1
  52. package/dist/RequestFiberSet.js +5 -5
  53. package/dist/Store/ContextMapContainer.d.ts +19 -3
  54. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  55. package/dist/Store/ContextMapContainer.js +13 -3
  56. package/dist/Store/Cosmos.d.ts.map +1 -1
  57. package/dist/Store/Cosmos.js +136 -68
  58. package/dist/Store/Disk.d.ts.map +1 -1
  59. package/dist/Store/Disk.js +24 -21
  60. package/dist/Store/Memory.d.ts +2 -2
  61. package/dist/Store/Memory.d.ts.map +1 -1
  62. package/dist/Store/Memory.js +26 -21
  63. package/dist/Store/SQL/Pg.d.ts +4 -0
  64. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  65. package/dist/Store/SQL/Pg.js +191 -0
  66. package/dist/Store/SQL/query.d.ts +38 -0
  67. package/dist/Store/SQL/query.d.ts.map +1 -0
  68. package/dist/Store/SQL/query.js +367 -0
  69. package/dist/Store/SQL.d.ts +20 -0
  70. package/dist/Store/SQL.d.ts.map +1 -0
  71. package/dist/Store/SQL.js +381 -0
  72. package/dist/Store/index.d.ts +4 -1
  73. package/dist/Store/index.d.ts.map +1 -1
  74. package/dist/Store/index.js +15 -3
  75. package/dist/Store/service.d.ts +16 -5
  76. package/dist/Store/service.d.ts.map +1 -1
  77. package/dist/Store/service.js +24 -6
  78. package/dist/adapters/ServiceBus.d.ts +6 -6
  79. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  80. package/dist/adapters/ServiceBus.js +9 -9
  81. package/dist/adapters/cosmos-client.d.ts +2 -2
  82. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  83. package/dist/adapters/cosmos-client.js +3 -3
  84. package/dist/adapters/logger.d.ts.map +1 -1
  85. package/dist/adapters/memQueue.d.ts +2 -2
  86. package/dist/adapters/memQueue.d.ts.map +1 -1
  87. package/dist/adapters/memQueue.js +3 -3
  88. package/dist/adapters/mongo-client.d.ts +2 -2
  89. package/dist/adapters/mongo-client.d.ts.map +1 -1
  90. package/dist/adapters/mongo-client.js +3 -3
  91. package/dist/adapters/redis-client.d.ts +3 -3
  92. package/dist/adapters/redis-client.d.ts.map +1 -1
  93. package/dist/adapters/redis-client.js +3 -3
  94. package/dist/api/ContextProvider.d.ts +6 -6
  95. package/dist/api/ContextProvider.d.ts.map +1 -1
  96. package/dist/api/ContextProvider.js +6 -6
  97. package/dist/api/internal/auth.d.ts +1 -1
  98. package/dist/api/internal/events.d.ts +2 -2
  99. package/dist/api/internal/events.d.ts.map +1 -1
  100. package/dist/api/internal/events.js +11 -7
  101. package/dist/api/layerUtils.d.ts +5 -5
  102. package/dist/api/layerUtils.d.ts.map +1 -1
  103. package/dist/api/layerUtils.js +5 -5
  104. package/dist/api/routing/middleware/RouterMiddleware.d.ts +3 -3
  105. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  106. package/dist/api/routing/middleware/middleware.d.ts +35 -1
  107. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  108. package/dist/api/routing/middleware/middleware.js +39 -1
  109. package/dist/api/routing.d.ts +1 -5
  110. package/dist/api/routing.d.ts.map +1 -1
  111. package/dist/api/routing.js +3 -2
  112. package/dist/api/setupRequest.d.ts +6 -3
  113. package/dist/api/setupRequest.d.ts.map +1 -1
  114. package/dist/api/setupRequest.js +11 -6
  115. package/dist/logger.d.ts.map +1 -1
  116. package/examples/query.ts +30 -26
  117. package/package.json +36 -18
  118. package/src/CUPS.ts +2 -2
  119. package/src/Emailer/service.ts +2 -2
  120. package/src/MainFiberSet.ts +2 -2
  121. package/src/Model/Repository/Registry.ts +33 -0
  122. package/src/Model/Repository/internal/internal.ts +76 -59
  123. package/src/Model/Repository/makeRepo.ts +7 -4
  124. package/src/Model/Repository/service.ts +6 -0
  125. package/src/Model/Repository.ts +1 -0
  126. package/src/Model/query/new-kid-interpreter.ts +2 -2
  127. package/src/Model.ts +1 -0
  128. package/src/Operations.ts +2 -2
  129. package/src/OperationsRepo.ts +2 -2
  130. package/src/QueueMaker/SQLQueue.ts +8 -7
  131. package/src/QueueMaker/memQueue.ts +2 -2
  132. package/src/QueueMaker/sbqueue.ts +2 -2
  133. package/src/RequestContext.ts +4 -4
  134. package/src/RequestFiberSet.ts +4 -4
  135. package/src/Store/ContextMapContainer.ts +41 -2
  136. package/src/Store/Cosmos.ts +350 -255
  137. package/src/Store/Disk.ts +37 -33
  138. package/src/Store/Memory.ts +29 -22
  139. package/src/Store/SQL/Pg.ts +321 -0
  140. package/src/Store/SQL/query.ts +409 -0
  141. package/src/Store/SQL.ts +674 -0
  142. package/src/Store/index.ts +17 -2
  143. package/src/Store/service.ts +31 -7
  144. package/src/adapters/ServiceBus.ts +8 -8
  145. package/src/adapters/cosmos-client.ts +2 -2
  146. package/src/adapters/memQueue.ts +2 -2
  147. package/src/adapters/mongo-client.ts +2 -2
  148. package/src/adapters/redis-client.ts +2 -2
  149. package/src/api/ContextProvider.ts +11 -11
  150. package/src/api/internal/events.ts +14 -9
  151. package/src/api/layerUtils.ts +8 -8
  152. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  153. package/src/api/routing/middleware/middleware.ts +43 -0
  154. package/src/api/routing.ts +3 -3
  155. package/src/api/setupRequest.ts +27 -7
  156. package/test/contextProvider.test.ts +11 -11
  157. package/test/controller.test.ts +12 -9
  158. package/test/dist/contextProvider.test.d.ts.map +1 -1
  159. package/test/dist/controller.test.d.ts.map +1 -1
  160. package/test/dist/date-query.test.d.ts.map +1 -0
  161. package/test/dist/fixtures.d.ts +20 -10
  162. package/test/dist/fixtures.d.ts.map +1 -1
  163. package/test/dist/fixtures.js +11 -9
  164. package/test/dist/query.test.d.ts.map +1 -1
  165. package/test/dist/rawQuery.test.d.ts.map +1 -1
  166. package/test/dist/requires.test.d.ts.map +1 -1
  167. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  168. package/test/dist/sql-store.test.d.ts.map +1 -0
  169. package/test/fixtures.ts +10 -8
  170. package/test/query.test.ts +209 -33
  171. package/test/rawQuery.test.ts +22 -18
  172. package/test/requires.test.ts +6 -5
  173. package/test/rpc-multi-middleware.test.ts +72 -3
  174. package/test/sql-store.test.ts +1064 -0
  175. package/test/validateSample.test.ts +12 -9
  176. package/tsconfig.json +0 -1
@@ -1,13 +1,13 @@
1
1
  import type { MailContent, MailData } from "@sendgrid/helpers/classes/mail.js"
2
2
  import type { ResponseError } from "@sendgrid/mail"
3
- import { Data, type Effect, type NonEmptyReadonlyArray, type Redacted, ServiceMap } from "effect-app"
3
+ import { Context, Data, type Effect, type NonEmptyReadonlyArray, type Redacted } from "effect-app"
4
4
  import type { Email } from "effect-app/Schema"
5
5
 
6
6
  export class SendMailError extends Data.TaggedError("SendMailError")<{
7
7
  readonly raw: Error | ResponseError
8
8
  }> {}
9
9
 
10
- export class Emailer extends ServiceMap.Opaque<Emailer, {
10
+ export class Emailer extends Context.Opaque<Emailer, {
11
11
  sendMail: (msg: EmailMsgOptionalFrom) => Effect.Effect<void, SendMailError>
12
12
  }>()("effect-app/Emailer") {}
13
13
 
@@ -1,4 +1,4 @@
1
- import { Effect, Fiber, FiberSet, Layer, ServiceMap } from "effect-app"
1
+ import { Context, Effect, Fiber, FiberSet, Layer } from "effect-app"
2
2
  import type {} from "effect/Scope"
3
3
  import { InfraLogger } from "./logger.js"
4
4
  import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.js"
@@ -62,7 +62,7 @@ const make = Effect.gen(function*() {
62
62
  * you should register these long running fibers in a FiberSet, and join them at the end of your main program.
63
63
  * This way any errors will blow up the main program instead of fibers dying unknowingly.
64
64
  */
65
- export class MainFiberSet extends ServiceMap.Service<MainFiberSet>()("MainFiberSet", { make }) {
65
+ export class MainFiberSet extends Context.Service<MainFiberSet>()("MainFiberSet", { make }) {
66
66
  static readonly Live = Layer.effect(this, this.make)
67
67
  static readonly JoinLive = this.asEffect().pipe(
68
68
  Effect.andThen((_) => _.join),
@@ -0,0 +1,33 @@
1
+ import { Context, Effect } from "effect-app"
2
+
3
+ export interface RegisteredRepository {
4
+ readonly seedNamespace: (namespace: string) => Effect.Effect<void>
5
+ }
6
+
7
+ const make = Effect.sync(() => {
8
+ const repos = new Map<string, RegisteredRepository>()
9
+ return {
10
+ register(modelName: string, repo: RegisteredRepository) {
11
+ repos.set(modelName, repo)
12
+ },
13
+ seedNamespace: (namespace: string) =>
14
+ Effect.suspend(() =>
15
+ Effect.forEach(
16
+ repos.values(),
17
+ (r) => r.seedNamespace(namespace),
18
+ { concurrency: "unbounded", discard: true }
19
+ )
20
+ ),
21
+ get entries(): ReadonlyMap<string, RegisteredRepository> {
22
+ return repos
23
+ }
24
+ }
25
+ })
26
+
27
+ export class RepositoryRegistry extends Context.Opaque<RepositoryRegistry, {
28
+ readonly register: (modelName: string, repo: RegisteredRepository) => void
29
+ readonly seedNamespace: (namespace: string) => Effect.Effect<void>
30
+ readonly entries: ReadonlyMap<string, RegisteredRepository>
31
+ }>()("effect-app/RepositoryRegistry", { make }) {}
32
+
33
+ export const RepositoryRegistryLive = RepositoryRegistry.toLayer(RepositoryRegistry.make)
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import type {} from "effect/Equal"
3
3
  import type {} from "effect/Hash"
4
- import { Array, Chunk, Effect, Equivalence, flow, type NonEmptyReadonlyArray, Option, pipe, Pipeable, PubSub, Result, S, SchemaAST, ServiceMap, Unify } from "effect-app"
4
+ import { Array, Chunk, Context, Effect, Equivalence, flow, type NonEmptyReadonlyArray, Option, pipe, Pipeable, PubSub, Result, S, SchemaAST, Unify } from "effect-app"
5
5
  import { toNonEmptyArray } from "effect-app/Array"
6
6
  import { NotFoundError } from "effect-app/client/errors"
7
7
  import { flatMapOption } from "effect-app/Effect"
@@ -55,14 +55,14 @@ export function makeRepoInternal<
55
55
 
56
56
  function make<RInitial = never, E = never, RPublish = never, RCtx = never>(
57
57
  args: [Evt] extends [never] ? {
58
- schemaContext?: ServiceMap.ServiceMap<RCtx>
58
+ schemaContext?: Context.Context<RCtx>
59
59
  makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
60
60
  config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
61
61
  partitionValue?: (e?: Encoded) => string
62
62
  }
63
63
  }
64
64
  : {
65
- schemaContext?: ServiceMap.ServiceMap<RCtx>
65
+ schemaContext?: Context.Context<RCtx>
66
66
  publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, RPublish>
67
67
  makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
68
68
  config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
@@ -72,12 +72,12 @@ export function makeRepoInternal<
72
72
  ) {
73
73
  return Effect
74
74
  .gen(function*() {
75
- const rctx: ServiceMap.ServiceMap<RCtx> = args.schemaContext ?? ServiceMap.empty() as any
75
+ const rctx: Context.Context<RCtx> = args.schemaContext ?? Context.empty() as any
76
76
  const provideRctx = Effect.provide(rctx)
77
77
  const encodeMany = flow(
78
78
  S.encodeEffect(S.Array(schema)),
79
79
  provideRctx,
80
- Effect.withSpan("encodeMany", {}, { captureStackTrace: false })
80
+ Effect.withSpan("encodeMany", { attributes: { itemType: name } }, { captureStackTrace: false })
81
81
  )
82
82
  const decode = flow(S.decodeEffect(schema), provideRctx)
83
83
  const decodeMany = flow(
@@ -113,11 +113,14 @@ export function makeRepoInternal<
113
113
  let ast = _.ast
114
114
  if (ast._tag === "Declaration") ast = ast.typeParameters[0]!
115
115
 
116
- // In v4, to get the encoded (from) side of a schema, use SchemaAST.toEncoded
117
116
  const pickIdFromAst = (a: SchemaAST.AST) => {
118
- const encoded = SchemaAST.toEncoded(a)
119
- if (SchemaAST.isObjects(encoded)) {
120
- const field = encoded.propertySignatures.find((_) => _.name === idKey)
117
+ // Unwrap Declaration (e.g. TaggedClass) to get the underlying Objects AST
118
+ let inner = a
119
+ if (inner._tag === "Declaration") inner = inner.typeParameters[0]!
120
+ // Pick from the original AST to preserve the full encoding chain (e.g. decodeTo transformations).
121
+ // Using toEncoded would lose transformation info needed to encode Type -> Encoded.
122
+ if (SchemaAST.isObjects(inner)) {
123
+ const field = inner.propertySignatures.find((_) => _.name === idKey)
121
124
  if (field) {
122
125
  return S.Struct({ [idKey]: S.make(field.type) }) as unknown as Codec<T, Encoded>
123
126
  }
@@ -161,7 +164,7 @@ export function makeRepoInternal<
161
164
  )
162
165
  })
163
166
 
164
- const find = Effect.fn("find")(function*(id: T[IdKey]) {
167
+ const find = Effect.fn("find", { attributes: { itemType: name } })(function*(id: T[IdKey]) {
165
168
  yield* Effect.annotateCurrentSpan({ itemId: id })
166
169
 
167
170
  return yield* flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
@@ -188,62 +191,71 @@ export function makeRepoInternal<
188
191
  Effect.andThen(saveAllE)
189
192
  )
190
193
 
191
- const saveAndPublish = Effect.fn("saveAndPublish")(function*(items: Iterable<T>, events: Iterable<Evt> = []) {
192
- const it = Chunk.fromIterable(items)
193
- const evts = [...events]
194
- yield* Effect.annotateCurrentSpan({ itemIds: [...Chunk.map(it, (_) => _[idKey])], events: evts.length })
195
- return yield* saveAll(it)
196
- .pipe(
197
- Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))),
198
- // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
199
- (_) => flatMapOption(_, pub),
200
- Effect.andThen(PubSub.publish(changeFeed, [Chunk.toArray(it), "save"] as [T[], "save" | "remove"])),
201
- Effect.asVoid
202
- )
203
- })
194
+ const saveAndPublish = Effect.fn("saveAndPublish", { attributes: { itemType: name } })(
195
+ function*(items: Iterable<T>, events: Iterable<Evt> = []) {
196
+ const it = Chunk.fromIterable(items)
197
+ const evts = [...events]
198
+ yield* Effect.annotateCurrentSpan({ itemIds: [...Chunk.map(it, (_) => _[idKey])], events: evts.length })
199
+ return yield* saveAll(it)
200
+ .pipe(
201
+ Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))),
202
+ // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
203
+ (_) => flatMapOption(_, pub),
204
+ Effect.andThen(PubSub.publish(changeFeed, [Chunk.toArray(it), "save"] as [T[], "save" | "remove"])),
205
+ Effect.asVoid
206
+ )
207
+ }
208
+ )
204
209
 
205
- const removeAndPublish = Effect.fn("removeAndPublish")(function*(a: Iterable<T>, events: Iterable<Evt> = []) {
206
- const { set } = yield* cms
207
- const it = [...a]
208
- const evts = [...events]
209
- yield* Effect.annotateCurrentSpan({ itemIds: it.map((_) => _[idKey]), eventCount: evts.length })
210
- const items = yield* encodeMany(it).pipe(Effect.orDie)
211
- if (Array.isReadonlyArrayNonEmpty(items)) {
212
- yield* store.batchRemove(
213
- items.map((_) => (_[idKey])),
214
- args.config?.partitionValue?.(items[0])
215
- )
216
- for (const e of items) {
217
- set(e[idKey], undefined)
218
- }
219
- yield* Effect
220
- .sync(() => toNonEmptyArray(evts))
221
- // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
222
- .pipe((_) => flatMapOption(_, pub))
210
+ const removeAndPublish = Effect.fn("removeAndPublish", { attributes: { itemType: name } })(
211
+ function*(a: Iterable<T>, events: Iterable<Evt> = []) {
212
+ const { set } = yield* cms
213
+ const it = [...a]
214
+ const evts = [...events]
215
+ yield* Effect.annotateCurrentSpan({ itemIds: it.map((_) => _[idKey]), eventCount: evts.length })
216
+ const items = yield* encodeMany(it).pipe(Effect.orDie)
217
+ if (Array.isReadonlyArrayNonEmpty(items)) {
218
+ yield* store.batchRemove(
219
+ items.map((_) => (_[idKey])),
220
+ args.config?.partitionValue?.(items[0])
221
+ )
222
+ for (const e of items) {
223
+ set(e[idKey], undefined)
224
+ }
225
+ yield* Effect
226
+ .sync(() => toNonEmptyArray(evts))
227
+ // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
228
+ .pipe((_) => flatMapOption(_, pub))
223
229
 
224
- yield* PubSub.publish(changeFeed, [it, "remove"] as [T[], "save" | "remove"])
230
+ yield* PubSub.publish(changeFeed, [it, "remove"] as [T[], "save" | "remove"])
231
+ }
225
232
  }
226
- })
233
+ )
227
234
 
228
- const removeById = Effect.fn("removeById")(function*(...ids: readonly T[IdKey][]) {
229
- if (!Array.isReadonlyArrayNonEmpty(ids)) {
230
- return
231
- }
232
- const { set } = yield* cms
233
- const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie)
234
- yield* Effect.annotateCurrentSpan({ itemIds: eids })
235
- yield* store.batchRemove(eids)
236
- for (const id of eids) {
237
- set(id, undefined)
235
+ const removeById = Effect.fn("removeById", { attributes: { itemType: name } })(
236
+ function*(...ids: readonly T[IdKey][]) {
237
+ if (!Array.isReadonlyArrayNonEmpty(ids)) {
238
+ return
239
+ }
240
+ const { set } = yield* cms
241
+ const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie)
242
+ yield* Effect.annotateCurrentSpan({ itemIds: eids })
243
+ yield* store.batchRemove(eids)
244
+ for (const id of eids) {
245
+ set(id, undefined)
246
+ }
247
+ yield* PubSub.publish(changeFeed, [[], "remove"] as [T[], "save" | "remove"])
238
248
  }
239
- yield* PubSub.publish(changeFeed, [[], "remove"] as [T[], "save" | "remove"])
240
- })
249
+ )
241
250
 
242
251
  const parseMany = (items: readonly PM[]) =>
243
252
  Effect
244
253
  .flatMap(cms, (cm) =>
245
254
  decodeMany(items.map((_) => mapReverse(_, cm.set)))
246
- .pipe(Effect.orDie, Effect.withSpan("parseMany", {}, { captureStackTrace: false })))
255
+ .pipe(
256
+ Effect.orDie,
257
+ Effect.withSpan("parseMany", { attributes: { itemType: name } }, { captureStackTrace: false })
258
+ ))
247
259
  const parseMany2 = <A, R>(
248
260
  items: readonly PM[],
249
261
  schema: S.Codec<A, Encoded, R>
@@ -254,7 +266,10 @@ export function makeRepoInternal<
254
266
  .decodeEffect(S.Array(schema))(
255
267
  items.map((_) => mapReverse(_, cm.set))
256
268
  )
257
- .pipe(Effect.orDie, Effect.withSpan("parseMany2", {}, { captureStackTrace: false })))
269
+ .pipe(
270
+ Effect.orDie,
271
+ Effect.withSpan("parseMany2", { attributes: { itemType: name } }, { captureStackTrace: false })
272
+ ))
258
273
  const filter = <U extends keyof Encoded = keyof Encoded>(args: FilterArgs<Encoded, U>) =>
259
274
  store
260
275
  .filter(
@@ -327,6 +342,7 @@ export function makeRepoInternal<
327
342
  : eff,
328
343
  Effect.withSpan("Repository.query [effect-app/infra]", {
329
344
  attributes: {
345
+ itemType: name,
330
346
  "repository.model_name": name,
331
347
  query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter }
332
348
  }
@@ -334,7 +350,7 @@ export function makeRepoInternal<
334
350
  )
335
351
  }) as any
336
352
 
337
- const validateSample = Effect.fn("validateSample")(function*(options?: {
353
+ const validateSample = Effect.fn("validateSample", { attributes: { itemType: name } })(function*(options?: {
338
354
  percentage?: number
339
355
  maxItems?: number
340
356
  }) {
@@ -401,6 +417,7 @@ export function makeRepoInternal<
401
417
  saveAndPublish,
402
418
  removeAndPublish,
403
419
  removeById,
420
+ seedNamespace: (namespace: string) => store.seedNamespace(namespace),
404
421
  validateSample,
405
422
  queryRaw<A, Out, QR>(schema: S.Codec<A, Out, QR>, q: Q.RawQuery<Encoded, Out>) {
406
423
  const dec = S.decodeEffect(S.Array(schema))
@@ -441,7 +458,7 @@ export function makeRepoInternal<
441
458
  // },
442
459
  save: (...xes: any[]) =>
443
460
  Effect.flatMap(encMany(xes), (_) => saveAllE(_)).pipe(
444
- Effect.withSpan("mapped.save", {}, { captureStackTrace: false })
461
+ Effect.withSpan("mapped.save", { attributes: { itemType: name } }, { captureStackTrace: false })
445
462
  )
446
463
  }
447
464
  }
@@ -7,11 +7,12 @@
7
7
  // import type { ParserEnv } from "effect-app/Schema/custom/Parser"
8
8
  import type {} from "effect/Equal"
9
9
  import type {} from "effect/Hash"
10
- import { Effect, type NonEmptyReadonlyArray, type S, type ServiceMap } from "effect-app"
10
+ import { type Context, Effect, type NonEmptyReadonlyArray, type S } from "effect-app"
11
11
  import type { StoreConfig, StoreMaker } from "../../Store.js"
12
12
  import type { FieldValues } from "../filter/types.js"
13
13
  import { type ExtendedRepository, extendRepo } from "./ext.js"
14
14
  import { makeRepoInternal } from "./internal/internal.js"
15
+ import { RepositoryRegistry } from "./Registry.js"
15
16
  import type { Repository } from "./service.js"
16
17
 
17
18
  export interface RepositoryOptions<
@@ -52,7 +53,7 @@ export interface RepositoryOptions<
52
53
  * Optional context to be provided to Schema decode/encode.
53
54
  * Useful for effectful transformations like XWithItems, where items is a transformation retrieving elements from another database table or other source.
54
55
  */
55
- schemaContext?: ServiceMap.ServiceMap<RCtx>
56
+ schemaContext?: Context.Context<RCtx>
56
57
 
57
58
  overrides?: (
58
59
  repo: Repository<T, Encoded, Evt, ItemType, IdKey, Exclude<RSchema, RCtx>, RPublish>
@@ -85,7 +86,7 @@ export const makeRepo: {
85
86
  ): Effect.Effect<
86
87
  ExtendedRepository<T, Encoded, Evt, ItemType, IdKey, Exclude<RSchema, RCtx>, RPublish>,
87
88
  E,
88
- RInitial | StoreMaker
89
+ RInitial | StoreMaker | RepositoryRegistry
89
90
  >
90
91
  <
91
92
  ItemType extends string,
@@ -104,7 +105,7 @@ export const makeRepo: {
104
105
  ): Effect.Effect<
105
106
  ExtendedRepository<T, Encoded, Evt, ItemType, "id", Exclude<RSchema, RCtx>, RPublish>,
106
107
  E,
107
- RInitial | StoreMaker
108
+ RInitial | StoreMaker | RepositoryRegistry
108
109
  >
109
110
  } = <
110
111
  ItemType extends string,
@@ -135,5 +136,7 @@ export const makeRepo: {
135
136
  let r = yield* mkRepo.make<RInitial, E, RPublish, RCtx>(options as any)
136
137
  if (options.overrides) r = options.overrides(r)
137
138
  const repo = extendRepo(r)
139
+ const registry = yield* RepositoryRegistry
140
+ registry.register(itemType, repo)
138
141
  return repo
139
142
  })
@@ -37,6 +37,12 @@ export interface Repository<
37
37
  raw: RawQuery<Encoded, Out>
38
38
  ) => Effect.Effect<readonly T[], S.SchemaError, R>
39
39
 
40
+ /**
41
+ * Explicitly seed a namespace. Primary is seeded eagerly on initialization.
42
+ * Non-primary namespaces must be seeded explicitly before use.
43
+ */
44
+ readonly seedNamespace: (namespace: string) => Effect.Effect<void>
45
+
40
46
  readonly query: {
41
47
  // ending with projection
42
48
  <
@@ -1,5 +1,6 @@
1
1
  export * from "./Repository/ext.js"
2
2
  export * from "./Repository/legacy.js"
3
3
  export { makeRepo } from "./Repository/makeRepo.js"
4
+ export * from "./Repository/Registry.js"
4
5
  export * from "./Repository/service.js"
5
6
  export * from "./Repository/validation.js"
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
- import { Array, identity, Match, Option, pipe, S } from "effect-app"
3
+ import { Array, identity, Match, Option, pipe, S, SchemaAST } from "effect-app"
4
4
  import { toNonEmptyArray } from "effect-app/Array"
5
5
  import { dropUndefinedT } from "effect-app/utils"
6
6
  import type { FilterResult } from "../filter/filterApi.js"
@@ -165,7 +165,7 @@ export const toFilter = <
165
165
  let select: (keyof TFieldValues | { key: string; subKeys: string[] })[] = []
166
166
  // TODO: support more complex (nested) schemas?
167
167
  if (schema) {
168
- const t = walkTransformation(schema.ast)
168
+ const t = walkTransformation(SchemaAST.toEncoded(schema.ast))
169
169
  if (S.AST.isObjects(t)) {
170
170
  select = t.propertySignatures.map((_) => _.name as string)
171
171
  for (const prop of t.propertySignatures) {
package/src/Model.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./Model/dsl.js"
2
2
  export * as Q from "./Model/query.js"
3
3
  export { makeRepo } from "./Model/Repository.js"
4
+ export { type RegisteredRepository, RepositoryRegistry, RepositoryRegistryLive } from "./Model/Repository.js"
package/src/Operations.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { reportError } from "@effect-app/infra/errorReporter"
2
2
  import { subHours } from "date-fns"
3
- import { Cause, copy, Duration, Effect, Exit, type Fiber, Layer, Option, S, Schedule, ServiceMap } from "effect-app"
3
+ import { Cause, Context, copy, Duration, Effect, Exit, type Fiber, Layer, Option, S, Schedule } from "effect-app"
4
4
  import { annotateLogscoped } from "effect-app/Effect"
5
5
  import { dual, pipe } from "effect-app/Function"
6
6
  import { Operation, OperationFailure, OperationId, type OperationProgress, OperationSuccess } from "effect-app/Operations"
@@ -189,7 +189,7 @@ const make = Effect.gen(function*() {
189
189
  }
190
190
  })
191
191
 
192
- export class Operations extends ServiceMap.Opaque<Operations>()("effect-app/Operations", { make }) {
192
+ export class Operations extends Context.Opaque<Operations>()("effect-app/Operations", { make }) {
193
193
  private static readonly CleanupLive = this
194
194
  .use((_) =>
195
195
  _.cleanup.pipe(
@@ -1,8 +1,8 @@
1
- import { Effect, ServiceMap } from "effect-app"
1
+ import { Context, Effect } from "effect-app"
2
2
  import { Operation } from "effect-app/Operations"
3
3
  import { makeRepo } from "./Model.js"
4
4
 
5
- export class OperationsRepo extends ServiceMap.Service<OperationsRepo>()(
5
+ export class OperationsRepo extends Context.Service<OperationsRepo>()(
6
6
  "OperationRepo",
7
7
  {
8
8
  make: Effect.gen(function*() {
@@ -3,13 +3,13 @@ import { reportNonInterruptedFailure } from "@effect-app/infra/QueueMaker/errors
3
3
  import { type QueueBase, QueueMeta } from "@effect-app/infra/QueueMaker/service"
4
4
  import { subMinutes } from "date-fns"
5
5
  import { Effect, Fiber, type NonEmptyReadonlyArray, Option, S, Tracer } from "effect-app"
6
- import type { NonEmptyString255 } from "effect-app/Schema"
6
+ import { type NonEmptyString255 } from "effect-app/Schema"
7
7
  import { pretty } from "effect-app/utils"
8
8
  import { SqlClient } from "effect/unstable/sql"
9
9
  import { SQLModel } from "../adapters/SQL.js"
10
10
  import { InfraLogger } from "../logger.js"
11
11
 
12
- export const QueueId = S.Number.pipe(S.brand("QueueId"))
12
+ export const QueueId = S.Finite.pipe(S.brand("QueueId"))
13
13
  export type QueueId = typeof QueueId.Type
14
14
 
15
15
  // TODO: let the model track and Auto Generate versionColumn on every update instead
@@ -78,14 +78,14 @@ export function makeSQLQueue<
78
78
 
79
79
  const q = {
80
80
  offer: Effect.fnUntraced(function*(body: Evt, meta: typeof QueueMeta.Type) {
81
- yield* queueRepo.insertVoid({
81
+ yield* queueRepo.insertVoid(Queue.insert.make({
82
82
  body,
83
83
  meta,
84
84
  name: queueName,
85
85
  processingAt: Option.none(),
86
86
  finishedAt: Option.none(),
87
87
  etag: crypto.randomUUID()
88
- })
88
+ }))
89
89
  }),
90
90
  take: Effect.gen(function*() {
91
91
  while (true) {
@@ -94,15 +94,16 @@ export function makeSQLQueue<
94
94
  const dec = yield* decodeDrain(first)
95
95
  const { createdAt, updatedAt, ...rest } = dec
96
96
  return yield* drainRepo.update(
97
- { ...rest, processingAt: Option.some(new Date()) } // auto in lib , etag: crypto.randomUUID()
97
+ Drain.update.make({ ...rest, processingAt: Option.some(new Date()) }) // auto in lib , etag: crypto.randomUUID()
98
98
  )
99
99
  }
100
100
  if (first) return first
101
101
  yield* Effect.sleep(250)
102
102
  }
103
103
  }),
104
- finish: ({ createdAt, updatedAt, ...q }: Drain) =>
105
- drainRepo.updateVoid({ ...q, finishedAt: Option.some(new Date()) }) // auto in lib , etag: crypto.randomUUID()
104
+ finish: Effect.fn(function*({ createdAt, updatedAt, ...q }: Drain) {
105
+ return yield* drainRepo.updateVoid(Drain.update.make({ ...q, finishedAt: Option.some(new Date()) })) // auto in lib , etag: crypto.randomUUID()
106
+ })
106
107
  }
107
108
  const queue = {
108
109
  publish: (...messages: NonEmptyReadonlyArray<Evt>) =>
@@ -25,10 +25,10 @@ export function makeMemQueue<
25
25
  const qDrain = yield* mem.getOrCreateQueue(queueDrainName)
26
26
 
27
27
  const wireSchema = S.Struct({ body: schema, meta: QueueMeta })
28
- const wireSchemaJson = S.fromJsonString(wireSchema)
28
+ const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
29
29
  const encodePublish = S.encodeEffect(wireSchemaJson)
30
30
  const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
31
- const drainWJson = S.fromJsonString(drainW)
31
+ const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
32
32
 
33
33
  const parseDrain = flow(S.decodeUnknownEffect(drainWJson), Effect.orDie)
34
34
 
@@ -21,10 +21,10 @@ export function makeServiceBusQueue<
21
21
  body: schema,
22
22
  meta: QueueMeta
23
23
  })
24
- const wireSchemaJson = S.fromJsonString(wireSchema)
24
+ const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
25
25
  const encodePublish = S.encodeEffect(wireSchemaJson)
26
26
  const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
27
- const drainWJson = S.fromJsonString(drainW)
27
+ const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
28
28
  const parseDrain = flow(S.decodeUnknownEffect(drainWJson), Effect.orDie)
29
29
 
30
30
  return Effect.gen(function*() {
@@ -1,11 +1,11 @@
1
- import { S, ServiceMap } from "effect-app"
1
+ import { Context, S } from "effect-app"
2
2
  import { UserProfileId } from "effect-app/ids"
3
3
  import { NonEmptyString255 } from "effect-app/Schema"
4
4
 
5
- export const Locale = S.Literal("en", "de")
5
+ export const Locale = S.Literals(["en", "de"])
6
6
  export type Locale = typeof Locale.Type
7
7
 
8
- export class LocaleRef extends ServiceMap.Reference("Locale", { defaultValue: (): Locale => "en" }) {}
8
+ export class LocaleRef extends Context.Reference("Locale", { defaultValue: (): Locale => "en" }) {}
9
9
 
10
10
  export class RequestContext extends S.ExtendedClass<
11
11
  RequestContext,
@@ -23,7 +23,7 @@ export class RequestContext extends S.ExtendedClass<
23
23
  /** @deprecated */
24
24
  userProfile: S.optional(S.Struct({ sub: UserProfileId })) //
25
25
  }) {
26
- // static Tag = ServiceMap.Tag<RequestContext>()
26
+ // static Tag = Context.Tag<RequestContext>()
27
27
 
28
28
  static toMonitoring(this: void, self: RequestContext) {
29
29
  return {
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Effect, Fiber, FiberSet, Layer, ServiceMap, type Tracer } from "effect-app"
2
+ import { Context, Effect, Fiber, FiberSet, Layer, Option, type Tracer } from "effect-app"
3
3
  import { reportRequestError, reportUnknownRequestError } from "./api/reportError.js"
4
4
  import { InfraLogger } from "./logger.js"
5
5
 
@@ -8,8 +8,8 @@ const getRootParentSpan = Effect.gen(function*() {
8
8
  Effect.catchTag("NoSuchElementError", () => Effect.succeed(null))
9
9
  )
10
10
  if (!span) return span
11
- while (span._tag === "Span" && span.parent !== undefined) {
12
- span = span.parent
11
+ while (span._tag === "Span" && Option.isSome(span.parent)) {
12
+ span = span.parent.value
13
13
  }
14
14
  return span
15
15
  })
@@ -92,7 +92,7 @@ const make = Effect.gen(function*() {
92
92
  * Whenever you fork a fiber for a Request, and you want to prevent dependent services to close prematurely on interruption,
93
93
  * like the ServiceBus Sender, you should register these fibers in this FiberSet.
94
94
  */
95
- export class RequestFiberSet extends ServiceMap.Service<RequestFiberSet>()("RequestFiberSet", { make }) {
95
+ export class RequestFiberSet extends Context.Service<RequestFiberSet>()("RequestFiberSet", { make }) {
96
96
  static readonly Live = Layer.effect(this, this.make)
97
97
  static readonly register = <A, E, R>(self: Effect.Effect<A, E, R>) =>
98
98
  this.asEffect().pipe(Effect.andThen((_) => _.register(self)))
@@ -1,4 +1,6 @@
1
- import { Data, Effect, Layer, ServiceMap } from "effect-app"
1
+ import { Context, Data, Effect, Layer, RequestResolver } from "effect-app"
2
+ import { dual } from "effect/Function"
3
+ import type * as Request from "effect/Request"
2
4
  import { ContextMap } from "./service.js"
3
5
 
4
6
  // TODO: we have to create a new contextmap on every request.
@@ -7,7 +9,7 @@ import { ContextMap } from "./service.js"
7
9
  // we can call another start after startup. but it would be even better if we could Die on accessing rootmap
8
10
  // we could also make the ContextMap optional, and when missing, issue a warning instead?
9
11
 
10
- export class ContextMapContainer extends ServiceMap.Reference("ContextMapContainer", {
12
+ export class ContextMapContainer extends Context.Reference("ContextMapContainer", {
11
13
  defaultValue: (): ContextMap | "root" => "root"
12
14
  }) {
13
15
  static readonly layer = Layer.effect(this, ContextMap.make.pipe(Effect.map(ContextMap.of)))
@@ -18,3 +20,40 @@ export class ContextMapNotStartedError extends Data.TaggedError("ContextMapNotSt
18
20
  export const getContextMap = ContextMapContainer.asEffect().pipe(
19
21
  Effect.filterOrFail((_) => _ !== "root", () => new ContextMapNotStartedError())
20
22
  )
23
+
24
+ /**
25
+ * Uses the official `RequestResolver.withCache` internally,
26
+ * creating one cached resolver per ContextMap (i.e. per request).
27
+ * Uses a shared semaphore in the ContextMap to ensure safe single initialization.
28
+ */
29
+ export const withRequestResolverCache: {
30
+ <A extends Request.Request<any, any>>(options: {
31
+ readonly capacity: number
32
+ readonly strategy?: "lru" | "fifo" | undefined
33
+ }): (
34
+ self: RequestResolver.RequestResolver<A>
35
+ ) => Effect.Effect<RequestResolver.RequestResolver<A>, ContextMapNotStartedError>
36
+ <A extends Request.Request<any, any>>(
37
+ self: RequestResolver.RequestResolver<A>,
38
+ options: {
39
+ readonly capacity: number
40
+ readonly strategy?: "lru" | "fifo" | undefined
41
+ }
42
+ ): Effect.Effect<RequestResolver.RequestResolver<A>, ContextMapNotStartedError>
43
+ } = dual(2, <A extends Request.Request<any, any>>(
44
+ self: RequestResolver.RequestResolver<A>,
45
+ options: {
46
+ readonly capacity: number
47
+ readonly strategy?: "lru" | "fifo" | undefined
48
+ }
49
+ ): Effect.Effect<RequestResolver.RequestResolver<A>, ContextMapNotStartedError> => {
50
+ const cacheKey = Symbol()
51
+ return getContextMap.pipe(
52
+ Effect.flatMap((ctxMap) =>
53
+ ctxMap.getOrCreateStoreEffect(
54
+ cacheKey,
55
+ RequestResolver.withCache(self, options)
56
+ )
57
+ )
58
+ )
59
+ })