/* eslint-disable @typescript-eslint/no-explicit-any */ import type { UniqueKey } from "@azure/cosmos" import { Context, Effect, type NonEmptyReadonlyArray, type Option, type Redacted } from "effect-app" import * as Semaphore from "effect/Semaphore" import type { OptimisticConcurrencyException } from "../errors.js" import type { FilterResult } from "../Model/filter/filterApi.js" import type { FieldValues } from "../Model/filter/types.js" import type { FieldPath } from "../Model/filter/types/path/index.js" import { type RawQuery } from "../Model/query.js" export interface StoreConfig { partitionValue: (e?: E) => string /** * Primarily used for testing, creating namespaces in the database to separate data e.g to run multiple tests in isolation within the same database. * Memory/Disk use separate store instances per namespace. CosmosDB uses namespace-prefixed partition keys. SQL uses a `_namespace` column. */ allowNamespace?: (namespace: string) => boolean /** * just in time migrations, supported by the database driver, supporting queries, for simple default values */ defaultValues?: Partial /** * How many items can be processed in one batch at a time. * Defaults to 100 for CosmosDB. */ maxBulkSize?: number /** * Unique indexes, mainly for CosmosDB */ uniqueKeys?: UniqueKey[] } export type SupportedValues = string | boolean | number | null export type SupportedValues2 = string | boolean | number // default is eq export type Where = | { key: string; t?: "eq" | "not-eq"; value: SupportedValues } | { key: string; t: "gt" | "lt" | "gte" | "lte"; value: SupportedValues2 } | { key: string t: "contains" | "starts-with" | "ends-with" | "not-contains" | "not-starts-with" | "not-ends-with" value: string } | { key: string; t: "includes" | "not-includes"; value: string } | { key: string t: "in" | "not-in" value: readonly (SupportedValues)[] } export type Filter = readonly FilterResult[] export interface O { key: FieldPath direction: "ASC" | "DESC" } export interface FilterArgs { t: Encoded filter?: Filter | undefined select?: NonEmptyReadonlyArray | undefined order?: NonEmptyReadonlyArray>> limit?: number | undefined skip?: number | undefined } export type FilterFunc = ( args: FilterArgs ) => Effect.Effect<(U extends undefined ? Encoded : Pick)[]> export interface Store< IdKey extends keyof Encoded, Encoded extends FieldValues, PM extends PersistenceModelType = PersistenceModelType > { all: Effect.Effect filter: FilterFunc find: (id: Encoded[IdKey]) => Effect.Effect> set: (e: PM) => Effect.Effect batchSet: ( items: NonEmptyReadonlyArray ) => Effect.Effect, OptimisticConcurrencyException> bulkSet: ( items: NonEmptyReadonlyArray ) => Effect.Effect, OptimisticConcurrencyException> batchRemove: (ids: NonEmptyReadonlyArray, partitionKey?: string) => Effect.Effect queryRaw: (query: RawQuery) => Effect.Effect /** * Explicitly seed a namespace. Primary is seeded eagerly on initialization. * Non-primary namespaces must be seeded explicitly before use. */ seedNamespace: (namespace: string) => Effect.Effect } export class StoreMaker extends Context.Opaque( name: string, idKey: IdKey, seed?: Effect.Effect, E, R>, config?: StoreConfig ) => Effect.Effect, E, R> }>()("effect-app/StoreMaker") { } export const makeContextMap = () => { const etags = new Map() const getEtag = (id: string) => etags.get(id) const setEtag = (id: string, eTag: string | undefined) => { if (eTag === undefined) { etags.delete(id) } else { etags.set(id, eTag) } } // const parsedCache = new Map< // Parser, // Map> // >() // const parserCache = new Map< // Parser, // (i: any) => These.These // >() // const setAndReturn = ( // p: Parser, // np: (i: I) => These.These // ) => { // parserCache.set(p, np) // return np // } // const parserEnv: ParserEnv = { // // TODO: lax: true would turn off refinement checks, may help on large payloads // // but of course removes confirming of validation rules (which may be okay for a database owned by the app, as we write safely) // lax: false, // cache: { // getOrSetParser: (p) => parserCache.get(p) ?? setAndReturn(p, (i) => parserEnv.cache!.getOrSet(i, p)), // getOrSetParsers: (parsers) => { // return Object.entries(parsers).reduce((prev, [k, v]) => { // prev[k] = parserEnv.cache!.getOrSetParser(v) // return prev // }, {} as any) // }, // getOrSet: (i, parse): any => { // const c = parsedCache.get(parse) // if (c) { // const f = c.get(i) // if (f) { // // console.log("$$$ cache hit", i) // return f // } else { // const nf = parse(i, parserEnv) // c.set(i, nf) // return nf // } // } else { // const nf = parse(i, parserEnv) // parsedCache.set(parse, new Map([[i, nf]])) // return nf // } // } // } // } const store = new Map() const sem = Semaphore.makeUnsafe(1) return { get: getEtag, set: setEtag, getOrCreateStore: (key: symbol, make: () => T): T => { let value = store.get(key) as T | undefined if (value === undefined) { value = make() store.set(key, value) } return value }, getOrCreateStoreEffect: (key: symbol, make: Effect.Effect): Effect.Effect => sem.withPermits(1)(Effect.uninterruptible(Effect.gen(function*() { const value = store.get(key) as T | undefined if (value !== undefined) return value const v = yield* make store.set(key, v) return v }))) } } const makeMap = Effect.sync(() => makeContextMap()) export class ContextMap extends Context.Opaque()("effect-app/ContextMap", { make: makeMap }) { } export type PersistenceModelType = Encoded & { _etag?: string | undefined } export interface StorageConfig { url: Redacted.Redacted prefix: string dbName: string }