Skip to content

Commit

Permalink
feat: context map container
Browse files Browse the repository at this point in the history
  • Loading branch information
patroza committed Nov 10, 2023
1 parent 1b292a7 commit e32dfd5
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-vans-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/infra": minor
---

context map container
13 changes: 6 additions & 7 deletions packages/infra/_src/services/Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { NotFoundError } from "../errors.js"
import { ContextMap } from "../services/Store.js"
import type { Filter } from "../services/Store.js"

export type RequestCTX = ContextMap
/**
* @tsplus type Repository
*/
Expand All @@ -19,24 +18,24 @@ export interface Repository<
ItemType extends string
> {
itemType: ItemType
find: (id: T["id"]) => Effect<RequestCTX, never, Option<T>>
all: Effect<ContextMap, never, T[]>
find: (id: T["id"]) => Effect<never, never, Option<T>>
all: Effect<never, never, T[]>
saveAndPublish: (
items: Iterable<T>,
events?: Iterable<Evt>
) => Effect<RequestCTX, InvalidStateError | OptimisticConcurrencyException, void>
) => Effect<never, InvalidStateError | OptimisticConcurrencyException, void>
removeAndPublish: (
items: Iterable<T>,
events?: Iterable<Evt>
) => Effect<RequestCTX, never, void>
) => Effect<never, never, void>
utils: {
mapReverse: (
pm: PM,
setEtag: (id: string, eTag: string | undefined) => void
) => unknown // TODO
parse: (a: unknown, env?: ParserEnv | undefined) => T
all: Effect<ContextMap, never, PM[]>
filter: (filter: Filter<PM>, cursor?: { limit?: number; skip?: number }) => Effect<ContextMap, never, PM[]>
all: Effect<never, never, PM[]>
filter: (filter: Filter<PM>, cursor?: { limit?: number; skip?: number }) => Effect<never, never, PM[]>
}
}

Expand Down
42 changes: 22 additions & 20 deletions packages/infra/_src/services/RepositoryBase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ParserEnv } from "@effect-app/schema/custom/Parser"
import type { Repository, RequestCTX } from "./Repository.js"
import { ContextMap, StoreMaker } from "./Store.js"
import type { Repository } from "./Repository.js"
import { StoreMaker } from "./Store.js"
import type { Filter, StoreConfig, Where } from "./Store.js"
import type {} from "effect/Equal"
import type {} from "effect/Hash"
Expand All @@ -10,6 +10,7 @@ import { makeFilters } from "@effect-app/infra/filter"
import type { Schema } from "@effect-app/prelude"
import { EParserFor } from "@effect-app/prelude/schema"
import type { InvalidStateError, OptimisticConcurrencyException } from "../errors.js"
import { ContextMapContainer } from "./Store/ContextMapContainer.js"

/**
* A base for creating an abstract class usable as Tag, Companion and interface to create your own implementation.
Expand All @@ -20,25 +21,25 @@ export const RepositoryBase = <Service>() => {
) => {
abstract class RepositoryBaseC implements Repository<T, PM, Evt, ItemType> {
itemType: ItemType = itemType
abstract find: (id: T["id"]) => Effect<RequestCTX, never, Opt<T>>
abstract all: Effect<ContextMap, never, T[]>
abstract find: (id: T["id"]) => Effect<never, never, Opt<T>>
abstract all: Effect<never, never, T[]>
abstract saveAndPublish: (
items: Iterable<T>,
events?: Iterable<Evt>
) => Effect<RequestCTX, InvalidStateError | OptimisticConcurrencyException, void>
) => Effect<never, InvalidStateError | OptimisticConcurrencyException, void>
abstract utils: {
mapReverse: (
pm: PM,
setEtag: (id: string, eTag: string | undefined) => void
) => unknown // TODO
parse: (a: unknown, env?: ParserEnv | undefined) => T
all: Effect<ContextMap, never, PM[]>
filter: (filter: Filter<PM>, cursor?: { limit?: number; skip?: number }) => Effect<ContextMap, never, PM[]>
all: Effect<never, never, PM[]>
filter: (filter: Filter<PM>, cursor?: { limit?: number; skip?: number }) => Effect<never, never, PM[]>
}
abstract removeAndPublish: (
items: Iterable<T>,
events?: Iterable<Evt>
) => Effect<RequestCTX, never, void>
) => Effect<never, never, void>
static where = makeWhere<PM>()
static flatMap<R1, E1, B>(f: (a: Service) => Effect<R1, E1, B>): Effect<Service | R1, E1, B> {
return Effect.flatMap(this as unknown as Tag<Service, Service>, f)
Expand Down Expand Up @@ -93,18 +94,19 @@ export function makeRepo<
const mkStore = makeStore<PM>()(name, schema, mapTo)

function make(
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<RequestCTX, never, void>,
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<never, never, void>,
makeInitial?: Effect<never, never, readonly T[]>,
config?: Omit<StoreConfig<PM>, "partitionValue"> & {
partitionValue?: (a: PM) => string
}
) {
return Do(($) => {
const store = $(mkStore(makeInitial, config))
const cms = $(ContextMapContainer)

const allE = store.all.flatMap((items) =>
Do(($) => {
const { set } = $(ContextMap)
const { set } = $(cms.get)
return items.map((_) => mapReverse(_, set))
})
)
Expand All @@ -118,7 +120,7 @@ export function makeRepo<
.find(id)
.flatMap((items) =>
Do(($) => {
const { set } = $(ContextMap)
const { set } = $(cms.get)
return items.map((_) => mapReverse(_, set))
})
)
Expand All @@ -132,7 +134,7 @@ export function makeRepo<
Effect(a.toNonEmptyArray)
.flatMapOpt((a) =>
Do(($) => {
const { get, set } = $(ContextMap)
const { get, set } = $(cms.get)
const items = a.mapNonEmpty((_) => mapToPersistenceModel(_, get))
const ret = $(store.batchSet(items))
ret.forEach((_) => set(_.id, _._etag))
Expand All @@ -150,7 +152,7 @@ export function makeRepo<

function removeAndPublish(a: Iterable<T>, events: Iterable<Evt> = []) {
return Effect.gen(function*($) {
const { get, set } = yield* $(ContextMap)
const { get, set } = yield* $(cms.get)
const items = a.toChunk.map(encode)
// TODO: we should have a batchRemove on store so the adapter can actually batch...
for (const e of items) {
Expand All @@ -174,8 +176,8 @@ export function makeRepo<
parse: Parser.for(schema).unsafe,
filter: store
.filter
.flow((_) => _.tap((items) => ContextMap.map(({ set }) => items.forEach((_) => set(_.id, _._etag))))),
all: store.all.tap((items) => ContextMap.map(({ set }) => items.forEach((_) => set(_.id, _._etag))))
.flow((_) => _.tap((items) => cms.get.map(({ set }) => items.forEach((_) => set(_.id, _._etag))))),
all: store.all.tap((items) => cms.get.map(({ set }) => items.forEach((_) => set(_.id, _._etag))))
},
itemType: name,
find,
Expand Down Expand Up @@ -312,12 +314,12 @@ export const RepositoryBaseImpl = <Service>() => {
mapTo: (e: E, etag: string | undefined) => PM
): (abstract new() => Repository<T, PM, Evt, ItemType>) & Tag<Service, Service> & {
make<R, E>(
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<RequestCTX, never, void>,
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<never, never, void>,
makeInitial?: Effect<R, E, readonly T[]>,
config?: Omit<StoreConfig<PM>, "partitionValue"> & {
partitionValue?: (a: PM) => string
}
): Effect<StoreMaker | R, E, Repository<T, PM, Evt, ItemType>>
): Effect<StoreMaker | ContextMapContainer | R, E, Repository<T, PM, Evt, ItemType>>
where: ReturnType<typeof makeWhere<PM>>
flatMap: <R1, E1, B>(f: (a: Service) => Effect<R1, E1, B>) => Effect<Service | R1, E1, B>
makeLayer: (svc: Service) => Layer<never, never, Service>
Expand Down Expand Up @@ -346,14 +348,14 @@ export const RepositoryDefaultImpl = <Service>() => {
impl: Repository<T, PM, Evt, ItemType>
): Repository<T, PM, Evt, ItemType>
make<R, E>(
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<RequestCTX, never, void>,
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<never, never, void>,
makeInitial?: Effect<R, E, readonly T[]>,
config?: Omit<StoreConfig<PM>, "partitionValue"> & {
partitionValue?: (a: PM) => string
}
): Effect<StoreMaker | R, E, Repository<T, PM, Evt, ItemType>>
toLayer<R, E>(
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<RequestCTX, never, void>,
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<never, never, void>,
makeInitial?: Effect<R, E, readonly T[]>,
config?: Omit<StoreConfig<PM>, "partitionValue"> & {
partitionValue?: (a: PM) => string
Expand All @@ -367,7 +369,7 @@ export const RepositoryDefaultImpl = <Service>() => {
} => {
return class extends RepositoryBaseImpl<Service>()<PM, Evt>()(itemType, schema, mapFrom, mapTo) {
static toLayer<R, E>(
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<RequestCTX, never, void>,
publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect<never, never, void>,
makeInitial?: Effect<R, E, readonly T[]>,
config?: Omit<StoreConfig<PM>, "partitionValue"> & {
partitionValue?: (a: PM) => string
Expand Down
49 changes: 49 additions & 0 deletions packages/infra/_src/services/Store/ContextMapContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ContextMap, makeContextMap } from "./service.js"

// TODO: we have to create a new contextmap on every request.
// we want to share one map during startup
// but we want to make sure we don't re-use the startup map after startup
// we can call another start after startup. but it would be even better if we could Die on accessing rootmap
// we could also make the ContextMap optional, and when missing, issue a warning instead?

/**
* @tsplus companion ContextMapContainer.Ops
*/
export abstract class ContextMapContainer extends TagClass<ContextMapContainer>() {
abstract readonly get: Effect<never, never, ContextMap>
abstract readonly start: Effect<never, never, void>
static get get(): Effect<ContextMapContainer, never, ContextMap> {
return ContextMapContainer.flatMap((_) => _.get)
}
static get getOption() {
return Effect
.contextWith((_: Context<never>) => Context.getOption(_, ContextMapContainer))
.flatMap((ctx) =>
ctx.isSome()
? ctx.value.get.map(Option.some)
: Effect(Option.none)
)
}
}

export class ContextMapContainerImpl extends ContextMapContainer {
#ref: FiberRef<ContextMap>
constructor() {
super()
this.#ref = FiberRef.unsafeMake<ContextMap>(makeContextMap())
}

override get get() {
return this.#ref.get
}

override start = ContextMap.Make.flatMap((_) => this.#ref.set(_))
}

/**
* @tsplus static ContextMapContainer.Ops live
*/
export const live = Effect.sync(() => new ContextMapContainerImpl()).toLayer(ContextMapContainer)

/** @tsplus static ContextMap.Ops Tag */
export const RCTag = Tag<ContextMap>()
12 changes: 7 additions & 5 deletions packages/infra/_src/services/Store/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,7 @@ export abstract class StoreMaker extends TagClass<StoreMaker>() {
) => Effect<R, E, Store<PM, Id>>
}

/**
* @tsplus static ContextMap.Ops Make
*/
export const makeMap = Effect.sync((): ContextMap => {
export const makeContextMap = (): ContextMap => {
const etags = new Map<string, string>()
const getEtag = (id: string) => etags.get(id)
const setEtag = (id: string, eTag: string | undefined) => {
Expand Down Expand Up @@ -159,7 +156,12 @@ export const makeMap = Effect.sync((): ContextMap => {
set: setEtag,
parserEnv
}
})
}

/**
* @tsplus static ContextMap.Ops Make
*/
export const makeMap = Effect.sync(() => makeContextMap())

/**
* @tsplus type ContextMap
Expand Down

0 comments on commit e32dfd5

Please sign in to comment.