@effect-app/infra 4.0.0-beta.11 → 4.0.0-beta.111
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.
- package/CHANGELOG.md +729 -0
- package/dist/CUPS.d.ts +3 -3
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +3 -3
- package/dist/Emailer/service.d.ts +3 -3
- package/dist/Emailer/service.d.ts.map +1 -1
- package/dist/Emailer/service.js +3 -3
- package/dist/MainFiberSet.d.ts +2 -2
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +3 -3
- package/dist/Model/Repository/internal/internal.d.ts +3 -3
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +21 -16
- package/dist/Model/Repository/makeRepo.d.ts +2 -2
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.js +1 -1
- package/dist/Model/Repository/validation.d.ts +5 -4
- package/dist/Model/Repository/validation.d.ts.map +1 -1
- package/dist/Model/query/dsl.d.ts +9 -9
- package/dist/Operations.d.ts +2 -2
- package/dist/Operations.d.ts.map +1 -1
- package/dist/Operations.js +3 -3
- package/dist/OperationsRepo.d.ts +2 -2
- package/dist/OperationsRepo.d.ts.map +1 -1
- package/dist/OperationsRepo.js +3 -3
- package/dist/QueueMaker/SQLQueue.d.ts +2 -4
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +8 -6
- package/dist/QueueMaker/errors.d.ts +1 -1
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +3 -3
- package/dist/QueueMaker/sbqueue.js +3 -3
- package/dist/RequestContext.d.ts +19 -14
- package/dist/RequestContext.d.ts.map +1 -1
- package/dist/RequestContext.js +5 -5
- package/dist/RequestFiberSet.d.ts +2 -2
- package/dist/RequestFiberSet.d.ts.map +1 -1
- package/dist/RequestFiberSet.js +5 -5
- package/dist/Store/ContextMapContainer.d.ts +19 -3
- package/dist/Store/ContextMapContainer.d.ts.map +1 -1
- package/dist/Store/ContextMapContainer.js +13 -3
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +136 -68
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +3 -4
- package/dist/Store/Memory.d.ts +2 -2
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +4 -4
- package/dist/Store/SQL/Pg.d.ts +4 -0
- package/dist/Store/SQL/Pg.d.ts.map +1 -0
- package/dist/Store/SQL/Pg.js +186 -0
- package/dist/Store/SQL/query.d.ts +38 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +367 -0
- package/dist/Store/SQL.d.ts +20 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +370 -0
- package/dist/Store/index.d.ts +4 -1
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +12 -2
- package/dist/Store/service.d.ts +11 -5
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +24 -6
- package/dist/adapters/ServiceBus.d.ts +6 -6
- package/dist/adapters/ServiceBus.d.ts.map +1 -1
- package/dist/adapters/ServiceBus.js +9 -9
- package/dist/adapters/cosmos-client.d.ts +2 -2
- package/dist/adapters/cosmos-client.d.ts.map +1 -1
- package/dist/adapters/cosmos-client.js +3 -3
- package/dist/adapters/logger.d.ts.map +1 -1
- package/dist/adapters/memQueue.d.ts +2 -2
- package/dist/adapters/memQueue.d.ts.map +1 -1
- package/dist/adapters/memQueue.js +3 -3
- package/dist/adapters/mongo-client.d.ts +2 -2
- package/dist/adapters/mongo-client.d.ts.map +1 -1
- package/dist/adapters/mongo-client.js +3 -3
- package/dist/adapters/redis-client.d.ts +3 -3
- package/dist/adapters/redis-client.d.ts.map +1 -1
- package/dist/adapters/redis-client.js +3 -3
- package/dist/api/ContextProvider.d.ts +6 -6
- package/dist/api/ContextProvider.d.ts.map +1 -1
- package/dist/api/ContextProvider.js +6 -6
- package/dist/api/internal/auth.d.ts +1 -1
- package/dist/api/internal/events.d.ts +2 -2
- package/dist/api/internal/events.d.ts.map +1 -1
- package/dist/api/internal/events.js +6 -4
- package/dist/api/layerUtils.d.ts +5 -5
- package/dist/api/layerUtils.d.ts.map +1 -1
- package/dist/api/layerUtils.js +5 -5
- package/dist/api/routing/middleware/RouterMiddleware.d.ts +3 -3
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.d.ts +35 -1
- package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.js +39 -1
- package/dist/api/routing.d.ts +1 -5
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +3 -2
- package/dist/api/setupRequest.d.ts +6 -3
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +11 -6
- package/dist/logger.d.ts.map +1 -1
- package/examples/query.ts +30 -26
- package/package.json +32 -18
- package/src/CUPS.ts +2 -2
- package/src/Emailer/service.ts +2 -2
- package/src/MainFiberSet.ts +2 -2
- package/src/Model/Repository/internal/internal.ts +75 -59
- package/src/Model/Repository/makeRepo.ts +2 -2
- package/src/Operations.ts +2 -2
- package/src/OperationsRepo.ts +2 -2
- package/src/QueueMaker/SQLQueue.ts +8 -7
- package/src/QueueMaker/memQueue.ts +2 -2
- package/src/QueueMaker/sbqueue.ts +2 -2
- package/src/RequestContext.ts +4 -4
- package/src/RequestFiberSet.ts +4 -4
- package/src/Store/ContextMapContainer.ts +41 -2
- package/src/Store/Cosmos.ts +352 -255
- package/src/Store/Disk.ts +2 -3
- package/src/Store/Memory.ts +4 -4
- package/src/Store/SQL/Pg.ts +328 -0
- package/src/Store/SQL/query.ts +409 -0
- package/src/Store/SQL.ts +686 -0
- package/src/Store/index.ts +15 -1
- package/src/Store/service.ts +26 -7
- package/src/adapters/ServiceBus.ts +8 -8
- package/src/adapters/cosmos-client.ts +2 -2
- package/src/adapters/memQueue.ts +2 -2
- package/src/adapters/mongo-client.ts +2 -2
- package/src/adapters/redis-client.ts +2 -2
- package/src/api/ContextProvider.ts +11 -11
- package/src/api/internal/events.ts +6 -5
- package/src/api/layerUtils.ts +8 -8
- package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
- package/src/api/routing/middleware/middleware.ts +43 -0
- package/src/api/routing.ts +3 -3
- package/src/api/setupRequest.ts +27 -7
- package/test/contextProvider.test.ts +11 -11
- package/test/controller.test.ts +12 -9
- package/test/dist/contextProvider.test.d.ts.map +1 -1
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/date-query.test.d.ts.map +1 -0
- package/test/dist/fixtures.d.ts +18 -8
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +11 -9
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/dist/requires.test.d.ts.map +1 -1
- package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
- package/test/dist/sql-store.test.d.ts.map +1 -0
- package/test/fixtures.ts +10 -8
- package/test/query.test.ts +162 -16
- package/test/rawQuery.test.ts +19 -17
- package/test/requires.test.ts +6 -5
- package/test/rpc-multi-middleware.test.ts +72 -3
- package/test/sql-store.test.ts +1064 -0
- package/test/validateSample.test.ts +1 -1
- package/tsconfig.json +0 -1
package/src/Store/index.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { Effect, Layer, Redacted } from "effect-app"
|
|
3
|
+
import type { SqlClient } from "effect/unstable/sql"
|
|
3
4
|
import { CosmosStoreLayer } from "./Cosmos.js"
|
|
4
5
|
import { DiskStoreLayer } from "./Disk.js"
|
|
5
6
|
import { MemoryStoreLive } from "./Memory.js"
|
|
6
7
|
// import { RedisStoreLayer } from "./Redis.js"
|
|
7
8
|
import type { StorageConfig } from "./service.js"
|
|
9
|
+
import { SQLiteStoreLayer } from "./SQL.js"
|
|
10
|
+
import { PgStoreLayer } from "./SQL/Pg.js"
|
|
8
11
|
|
|
9
|
-
export function StoreMakerLayer(
|
|
12
|
+
export function StoreMakerLayer(
|
|
13
|
+
cfg: StorageConfig,
|
|
14
|
+
options?: { makeSqlClientLayer?: (namespace: string) => Layer.Layer<SqlClient.SqlClient> }
|
|
15
|
+
) {
|
|
10
16
|
return Effect
|
|
11
17
|
.sync(() => {
|
|
12
18
|
const storageUrl = Redacted.value(cfg.url)
|
|
@@ -19,6 +25,14 @@ export function StoreMakerLayer(cfg: StorageConfig) {
|
|
|
19
25
|
console.log("Using disk store at " + dir)
|
|
20
26
|
return DiskStoreLayer(cfg, dir)
|
|
21
27
|
}
|
|
28
|
+
if (storageUrl.startsWith("sqlite://")) {
|
|
29
|
+
console.log("Using SQLite store")
|
|
30
|
+
return SQLiteStoreLayer(cfg, options)
|
|
31
|
+
}
|
|
32
|
+
if (storageUrl.startsWith("pg://")) {
|
|
33
|
+
console.log("Using PostgreSQL store")
|
|
34
|
+
return PgStoreLayer(cfg)
|
|
35
|
+
}
|
|
22
36
|
// if (storageUrl.startsWith("redis://")) {
|
|
23
37
|
// console.log("Using Redis store")
|
|
24
38
|
// return RedisStoreLayer(cfg)
|
package/src/Store/service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import type { UniqueKey } from "@azure/cosmos"
|
|
3
|
-
import { Effect, type NonEmptyReadonlyArray, type Option, type Redacted
|
|
3
|
+
import { Context, Effect, type NonEmptyReadonlyArray, type Option, type Redacted } from "effect-app"
|
|
4
|
+
import * as Semaphore from "effect/Semaphore"
|
|
4
5
|
import type { OptimisticConcurrencyException } from "../errors.js"
|
|
5
6
|
import type { FilterResult } from "../Model/filter/filterApi.js"
|
|
6
7
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
@@ -10,8 +11,8 @@ import { type RawQuery } from "../Model/query.js"
|
|
|
10
11
|
export interface StoreConfig<E> {
|
|
11
12
|
partitionValue: (e?: E) => string
|
|
12
13
|
/**
|
|
13
|
-
* Primarily used for testing, creating namespaces in the database to separate data e.g to run multiple tests in isolation within the same database
|
|
14
|
-
*
|
|
14
|
+
* Primarily used for testing, creating namespaces in the database to separate data e.g to run multiple tests in isolation within the same database.
|
|
15
|
+
* Memory/Disk use separate store instances per namespace. CosmosDB uses namespace-prefixed partition keys. SQL uses a `_namespace` column.
|
|
15
16
|
*/
|
|
16
17
|
allowNamespace?: (namespace: string) => boolean
|
|
17
18
|
/**
|
|
@@ -89,7 +90,7 @@ export interface Store<
|
|
|
89
90
|
queryRaw: <Out>(query: RawQuery<Encoded, Out>) => Effect.Effect<readonly Out[]>
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
export class StoreMaker extends
|
|
93
|
+
export class StoreMaker extends Context.Opaque<StoreMaker, {
|
|
93
94
|
make: <IdKey extends keyof Encoded, Encoded extends FieldValues, R = never, E = never>(
|
|
94
95
|
name: string,
|
|
95
96
|
idKey: IdKey,
|
|
@@ -161,16 +162,34 @@ export const makeContextMap = () => {
|
|
|
161
162
|
// }
|
|
162
163
|
// }
|
|
163
164
|
|
|
165
|
+
const store = new Map<symbol, unknown>()
|
|
166
|
+
const sem = Semaphore.makeUnsafe(1)
|
|
167
|
+
|
|
164
168
|
return {
|
|
165
169
|
get: getEtag,
|
|
166
|
-
set: setEtag
|
|
167
|
-
|
|
170
|
+
set: setEtag,
|
|
171
|
+
getOrCreateStore: <T>(key: symbol, make: () => T): T => {
|
|
172
|
+
let value = store.get(key) as T | undefined
|
|
173
|
+
if (value === undefined) {
|
|
174
|
+
value = make()
|
|
175
|
+
store.set(key, value)
|
|
176
|
+
}
|
|
177
|
+
return value
|
|
178
|
+
},
|
|
179
|
+
getOrCreateStoreEffect: <T, E, R>(key: symbol, make: Effect.Effect<T, E, R>): Effect.Effect<T, E, R> =>
|
|
180
|
+
sem.withPermits(1)(Effect.uninterruptible(Effect.gen(function*() {
|
|
181
|
+
const value = store.get(key) as T | undefined
|
|
182
|
+
if (value !== undefined) return value
|
|
183
|
+
const v = yield* make
|
|
184
|
+
store.set(key, v)
|
|
185
|
+
return v
|
|
186
|
+
})))
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
const makeMap = Effect.sync(() => makeContextMap())
|
|
172
191
|
|
|
173
|
-
export class ContextMap extends
|
|
192
|
+
export class ContextMap extends Context.Opaque<ContextMap>()("effect-app/ContextMap", { make: makeMap }) {
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
export type PersistenceModelType<Encoded extends object> = Encoded & {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
|
|
2
2
|
import { type OperationOptionsBase, type ProcessErrorArgs, ServiceBusClient, type ServiceBusMessage, type ServiceBusMessageBatch, type ServiceBusReceivedMessage, type ServiceBusReceiver } from "@azure/service-bus"
|
|
3
|
-
import { Cause, Effect, Exit, FiberSet, Layer, type Scope
|
|
3
|
+
import { Cause, Context, Effect, Exit, FiberSet, Layer, type Scope } from "effect-app"
|
|
4
4
|
import { InfraLogger } from "../logger.js"
|
|
5
5
|
|
|
6
6
|
const withSpanAndLog = (name: string) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
@@ -19,7 +19,7 @@ function makeClient(url: string) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export class ServiceBusClientTag
|
|
22
|
-
extends
|
|
22
|
+
extends Context.Opaque<ServiceBusClientTag, ServiceBusClient>()("@services/Client", { make: makeClient })
|
|
23
23
|
{
|
|
24
24
|
static readonly layer = (url: string) => this.toLayer(this.make(url))
|
|
25
25
|
}
|
|
@@ -50,7 +50,7 @@ const makeSender = (name: string) =>
|
|
|
50
50
|
return { name, sendMessages }
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
export class Sender extends
|
|
53
|
+
export class Sender extends Context.Opaque<Sender, {
|
|
54
54
|
name: string
|
|
55
55
|
sendMessages: (
|
|
56
56
|
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
@@ -61,12 +61,12 @@ export class Sender extends ServiceMap.Opaque<Sender, {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export const SenderTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
64
|
-
const tag =
|
|
64
|
+
const tag = Context.Service<Id, Sender>(`ServiceBus.Sender.${queueName}`)
|
|
65
65
|
|
|
66
66
|
return Object.assign(tag, {
|
|
67
67
|
layer: Layer.effect(
|
|
68
68
|
tag,
|
|
69
|
-
Sender.make(queueName).pipe(Effect.map(
|
|
69
|
+
Sender.make(queueName).pipe(Effect.map(Sender.of))
|
|
70
70
|
)
|
|
71
71
|
})
|
|
72
72
|
}
|
|
@@ -163,7 +163,7 @@ const makeReceiver = (name: string) =>
|
|
|
163
163
|
}
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
-
export class Receiver extends
|
|
166
|
+
export class Receiver extends Context.Opaque<Receiver, {
|
|
167
167
|
name: string
|
|
168
168
|
make: (waitTillEmpty: Effect.Effect<void>) => Effect.Effect<ServiceBusReceiver, never, Scope.Scope>
|
|
169
169
|
makeSession: (
|
|
@@ -180,12 +180,12 @@ export class Receiver extends ServiceMap.Opaque<Receiver, {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export const ReceiverTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
183
|
-
const tag =
|
|
183
|
+
const tag = Context.Service<Id, Receiver>(`ServiceBus.Receiver.${queueName}`)
|
|
184
184
|
|
|
185
185
|
return Object.assign(tag, {
|
|
186
186
|
layer: Layer.effect(
|
|
187
187
|
tag,
|
|
188
|
-
makeReceiver(queueName).pipe(Effect.map(
|
|
188
|
+
makeReceiver(queueName).pipe(Effect.map(Receiver.of))
|
|
189
189
|
)
|
|
190
190
|
})
|
|
191
191
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CosmosClient as ComosClient_ } from "@azure/cosmos"
|
|
2
|
-
import { Effect, Layer
|
|
2
|
+
import { Context, Effect, Layer } from "effect-app"
|
|
3
3
|
|
|
4
4
|
const withClient = (url: string) => Effect.sync(() => new ComosClient_(url))
|
|
5
5
|
|
|
6
6
|
export const makeCosmosClient = (url: string, dbName: string) =>
|
|
7
7
|
Effect.map(withClient(url), (x) => ({ db: x.database(dbName) }))
|
|
8
8
|
|
|
9
|
-
export class CosmosClient extends
|
|
9
|
+
export class CosmosClient extends Context.Service<CosmosClient, {
|
|
10
10
|
readonly db: ReturnType<InstanceType<typeof ComosClient_>["database"]>
|
|
11
11
|
}>()("@services/CosmosClient") {}
|
|
12
12
|
|
package/src/adapters/memQueue.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, type Queue
|
|
1
|
+
import { Context, Effect, type Queue } from "effect-app"
|
|
2
2
|
import * as Q from "effect/Queue"
|
|
3
3
|
|
|
4
4
|
const make = Effect
|
|
@@ -16,6 +16,6 @@ const make = Effect
|
|
|
16
16
|
}
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
export class MemQueue extends
|
|
19
|
+
export class MemQueue extends Context.Opaque<MemQueue>()("effect-app/MemQueue", { make }) {
|
|
20
20
|
static readonly Live = this.toLayer(this.make)
|
|
21
21
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Layer
|
|
1
|
+
import { Context, Effect, Layer } from "effect-app"
|
|
2
2
|
import { MongoClient as MongoClient_ } from "mongodb"
|
|
3
3
|
|
|
4
4
|
// TODO: we should probably share a single client...
|
|
@@ -15,7 +15,7 @@ const withClient = (url: string) =>
|
|
|
15
15
|
|
|
16
16
|
const makeMongoClient = (url: string, dbName?: string) => Effect.map(withClient(url), (x) => ({ db: x.db(dbName) }))
|
|
17
17
|
|
|
18
|
-
export class MongoClient extends
|
|
18
|
+
export class MongoClient extends Context.Service<MongoClient, {
|
|
19
19
|
readonly db: ReturnType<InstanceType<typeof MongoClient_>["db"]>
|
|
20
20
|
}>()("@services/MongoClient") {}
|
|
21
21
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Data, Effect, Layer, Option
|
|
1
|
+
import { Context, Data, Effect, Layer, Option } from "effect-app"
|
|
2
2
|
import type { RedisClient as Client } from "redis"
|
|
3
3
|
import Redlock from "redlock"
|
|
4
4
|
|
|
@@ -90,7 +90,7 @@ export const makeRedisClient = (makeClient: () => Client) =>
|
|
|
90
90
|
.pipe(Effect.uninterruptible, Effect.orDie)
|
|
91
91
|
)
|
|
92
92
|
|
|
93
|
-
export class RedisClient extends
|
|
93
|
+
export class RedisClient extends Context.Service<RedisClient, {
|
|
94
94
|
readonly client: Client
|
|
95
95
|
readonly lock: Redlock
|
|
96
96
|
readonly get: (key: string) => Effect.Effect<Option.Option<string>, ConnectionException>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { Effect, Layer, type NonEmptyReadonlyArray, pipe, type Scope
|
|
2
|
+
import { Context, Effect, Layer, type NonEmptyReadonlyArray, pipe, type Scope } from "effect-app"
|
|
3
3
|
|
|
4
4
|
import { type HttpRouter } from "effect-app/http"
|
|
5
5
|
import { type EffectGenUtils } from "effect-app/utils/gen"
|
|
@@ -23,7 +23,7 @@ type TDepsArr<TDeps extends ReadonlyArray<any>> = {
|
|
|
23
23
|
// E = never => the context provided cannot trigger errors
|
|
24
24
|
// TODO: remove HttpLayerRouter.Provided - it's not even relevant outside of Http context, while ContextProviders are for anywhere. Only support Scope.Scope?
|
|
25
25
|
// _R extends HttpLayerRouter.Provided => the context provided can only have what HttpLayerRouter.Provided provides as requirements
|
|
26
|
-
ContextTagWithDefault.Base<Effect.Effect<
|
|
26
|
+
ContextTagWithDefault.Base<Effect.Effect<Context.Context<infer _1>, never, infer _R>> // & { _tag: infer _2 }>
|
|
27
27
|
? [_R] extends [HttpRouter.Provided] ? TDeps[K]
|
|
28
28
|
: `HttpLayerRouter.Provided is the only requirement ${TDeps[K]["Service"][
|
|
29
29
|
"_tag"
|
|
@@ -58,9 +58,9 @@ export const mergeContextProviders = <
|
|
|
58
58
|
Effect.Effect<
|
|
59
59
|
// we need to merge all contexts into one
|
|
60
60
|
// v4: Service.Shape extracts the service value type (v3's Tag.Identifier)
|
|
61
|
-
|
|
61
|
+
Context.Context<GetContext<EffectGenUtils.Success<Context.Service.Shape<TDeps[number]>>>>,
|
|
62
62
|
never,
|
|
63
|
-
EffectGenUtils.
|
|
63
|
+
EffectGenUtils.Context<Context.Service.Shape<TDeps[number]>>
|
|
64
64
|
>,
|
|
65
65
|
LayerUtils.GetLayersError<{ [K in keyof TDeps]: TDeps[K]["Default"] }>,
|
|
66
66
|
LayerUtils.GetLayersSuccess<{ [K in keyof TDeps]: TDeps[K]["Default"] }>
|
|
@@ -78,14 +78,14 @@ export const mergeContextProviders = <
|
|
|
78
78
|
handle: handle[Symbol.toStringTag] === "GeneratorFunction" ? Effect.fnUntraced(handle)() : handle
|
|
79
79
|
}
|
|
80
80
|
))
|
|
81
|
-
// services are effects which return some
|
|
81
|
+
// services are effects which return some Context.Context<...>
|
|
82
82
|
const context = yield* mergeContexts(services as any)
|
|
83
83
|
return context
|
|
84
84
|
})
|
|
85
85
|
}) as any
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
// Effect Rpc Middleware: for single tag providing, we could use Provides, for providing
|
|
88
|
+
// Effect Rpc Middleware: for single tag providing, we could use Provides, for providing Context or Layer (bad boy) we could use Wrap..
|
|
89
89
|
export const ContextProvider = <
|
|
90
90
|
ContextProviderA,
|
|
91
91
|
MakeContextProviderE,
|
|
@@ -107,7 +107,7 @@ export const ContextProvider = <
|
|
|
107
107
|
dependencies?: Dependencies
|
|
108
108
|
}
|
|
109
109
|
) => {
|
|
110
|
-
const ctx =
|
|
110
|
+
const ctx = Context.Service<
|
|
111
111
|
ContextProviderId,
|
|
112
112
|
Effect.Effect<ContextProviderA, never, ContextProviderR>
|
|
113
113
|
>(
|
|
@@ -146,17 +146,17 @@ export const MergedContextProvider = <
|
|
|
146
146
|
Effect.Effect<
|
|
147
147
|
// we need to merge all contexts into one
|
|
148
148
|
// v4: Service.Shape extracts the service value type (v3's Tag.Identifier)
|
|
149
|
-
|
|
149
|
+
Context.Context<GetContext<EffectGenUtils.Success<Context.Service.Shape<TDeps[number]>>>>,
|
|
150
150
|
never,
|
|
151
|
-
EffectGenUtils.
|
|
151
|
+
EffectGenUtils.Context<Context.Service.Shape<TDeps[number]>>
|
|
152
152
|
>,
|
|
153
153
|
LayerUtils.GetLayersError<{ [K in keyof TDeps]: TDeps[K]["Default"] }>,
|
|
154
154
|
// v4: Identifier here is correct — it's the nominal service identity for layer provide/exclude
|
|
155
155
|
| Exclude<
|
|
156
|
-
|
|
156
|
+
Context.Service.Identifier<TDeps[number]>,
|
|
157
157
|
LayerUtils.GetLayersSuccess<{ [K in keyof TDeps]: TDeps[K]["Default"] }>
|
|
158
158
|
>
|
|
159
159
|
| LayerUtils.GetLayersContext<{ [K in keyof TDeps]: TDeps[K]["Default"] }>
|
|
160
160
|
>
|
|
161
161
|
|
|
162
|
-
export const EmptyContextProvider = ContextProvider({ effect: Effect.succeed(Effect.succeed(
|
|
162
|
+
export const EmptyContextProvider = ContextProvider({ effect: Effect.succeed(Effect.succeed(Context.empty())) })
|
|
@@ -9,14 +9,14 @@ const keepAlive = Stream.fromEffectSchedule(Effect.succeed(":keep-alive"), Sched
|
|
|
9
9
|
|
|
10
10
|
let connId = BigInt(0)
|
|
11
11
|
|
|
12
|
-
export const makeSSE = <A extends { id: any }, SI,
|
|
13
|
-
schema: S.Codec<A, SI,
|
|
12
|
+
export const makeSSE = <A extends { id: any }, SI, SRD, SRE>(
|
|
13
|
+
schema: S.Codec<A, SI, SRD, SRE>
|
|
14
14
|
) =>
|
|
15
15
|
<E, R>(events: Stream.Stream<{ evt: A; namespace: string }, E, R>) =>
|
|
16
16
|
Effect
|
|
17
17
|
.gen(function*() {
|
|
18
18
|
const id = connId++
|
|
19
|
-
const ctx = yield* Effect.
|
|
19
|
+
const ctx = yield* Effect.context<R | SRD | SRE>()
|
|
20
20
|
const res = HttpServerResponse.stream(
|
|
21
21
|
// workaround for different scoped behaviour for streams in Bun
|
|
22
22
|
// https://discord.com/channels/795981131316985866/1098177242598756412/1389646879675125861
|
|
@@ -28,7 +28,7 @@ export const makeSSE = <A extends { id: any }, SI, SR>(
|
|
|
28
28
|
|
|
29
29
|
const enc = new TextEncoder()
|
|
30
30
|
|
|
31
|
-
const encode = S.encodeEffect(S.fromJsonString(schema))
|
|
31
|
+
const encode = S.encodeEffect(S.fromJsonString(S.toCodecJson(schema)))
|
|
32
32
|
|
|
33
33
|
const eventStream = Stream.mapEffect(
|
|
34
34
|
events,
|
|
@@ -40,7 +40,8 @@ export const makeSSE = <A extends { id: any }, SI, SR>(
|
|
|
40
40
|
const stream = pipe(
|
|
41
41
|
setRetry,
|
|
42
42
|
Stream.merge(keepAlive),
|
|
43
|
-
|
|
43
|
+
// Keep this unary so pipe receives a function, not a Stream value.
|
|
44
|
+
(self) => Stream.merge(self, eventStream, { haltStrategy: "either" }),
|
|
44
45
|
Stream.tapCause((cause) => Effect.logError("SSE error", cause)),
|
|
45
46
|
Stream.map((_) => enc.encode(_ + "\n\n"))
|
|
46
47
|
)
|
package/src/api/layerUtils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { Effect, type Layer, type NonEmptyReadonlyArray, Option
|
|
2
|
+
import { Context, Effect, type Layer, type NonEmptyReadonlyArray, Option } from "effect-app"
|
|
3
3
|
import { InfraLogger } from "../logger.js"
|
|
4
4
|
|
|
5
5
|
// TODO: These LayerUtils are flaky, like in dependencies as a readonly array, it breaks when there are two entries
|
|
@@ -27,7 +27,7 @@ export namespace LayerUtils {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export type ContextTagWithDefault<Id, A, LayerE, LayerR> =
|
|
30
|
-
&
|
|
30
|
+
& Context.Service<Id, A>
|
|
31
31
|
& {
|
|
32
32
|
Default: Layer.Layer<Id, LayerE, LayerR>
|
|
33
33
|
}
|
|
@@ -36,29 +36,29 @@ export namespace ContextTagWithDefault {
|
|
|
36
36
|
export type Base<A> = ContextTagWithDefault<any, A, any, any>
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export type GetContext<T> = T extends
|
|
39
|
+
export type GetContext<T> = T extends Context.Context<infer Y> ? Y : never
|
|
40
40
|
|
|
41
41
|
export const mergeContexts = Effect.fnUntraced(
|
|
42
42
|
function*<
|
|
43
43
|
T extends readonly {
|
|
44
44
|
maker: any
|
|
45
|
-
handle: Effect.Effect<
|
|
45
|
+
handle: Effect.Effect<Context.Context<any> | Option.Option<Context.Context<any>>>
|
|
46
46
|
}[]
|
|
47
47
|
>(
|
|
48
48
|
makers: T
|
|
49
49
|
) {
|
|
50
|
-
let context =
|
|
50
|
+
let context = Context.empty()
|
|
51
51
|
for (const mw of makers) {
|
|
52
52
|
const ctx = yield* mw.handle.pipe(Effect.provide(context))
|
|
53
|
-
const moreContext =
|
|
53
|
+
const moreContext = Context.isContext(ctx) ? Option.some(ctx) : ctx
|
|
54
54
|
yield* InfraLogger.logDebug(
|
|
55
55
|
"Built dynamic context for middleware" + (mw.maker.key ?? mw.maker),
|
|
56
56
|
Option.map(moreContext, (c) => (c as any).toJSON().services)
|
|
57
57
|
)
|
|
58
58
|
if (moreContext.value) {
|
|
59
|
-
context =
|
|
59
|
+
context = Context.merge(context, moreContext.value)
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
return context as
|
|
62
|
+
return context as Context.Context<Effect.Success<T[number]["handle"]>>
|
|
63
63
|
}
|
|
64
64
|
)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
-
import { type
|
|
4
|
+
import { type Context, type Layer } from "effect-app"
|
|
5
5
|
import { type GetContextConfig, type RpcContextMap } from "effect-app/rpc/RpcContextMap"
|
|
6
6
|
import { type RpcMiddlewareV4 } from "effect-app/rpc/RpcMiddleware"
|
|
7
7
|
// module:
|
|
8
8
|
//
|
|
9
9
|
|
|
10
|
-
// v4: middleware tags are
|
|
10
|
+
// v4: middleware tags are Context.Service (not Effect) — they carry the RpcMiddlewareV4 as their service Shape
|
|
11
11
|
export type RouterMiddleware<
|
|
12
12
|
Self,
|
|
13
13
|
RequestContextMap extends Record<string, RpcContextMap.Any>, // what services will the middlware provide dynamically to the next, or raise errors.
|
|
@@ -18,9 +18,9 @@ export type RouterMiddleware<
|
|
|
18
18
|
_ContextProviderR, // what the context provider requires
|
|
19
19
|
RequestContextId
|
|
20
20
|
> =
|
|
21
|
-
&
|
|
21
|
+
& Context.Service<Self, RpcMiddlewareV4<ContextProviderA, ContextProviderE, never>>
|
|
22
22
|
& {
|
|
23
23
|
readonly Default: Layer.Layer<Self, MakeMiddlewareE, MakeMiddlewareR>
|
|
24
|
-
readonly requestContext:
|
|
24
|
+
readonly requestContext: Context.Service<RequestContextId, GetContextConfig<RequestContextMap>>
|
|
25
25
|
readonly requestContextMap: RequestContextMap
|
|
26
26
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { Cause, Config, Effect, Layer, Schema } from "effect"
|
|
3
3
|
import { ConfigureInterruptibilityMiddleware, DevMode, DevModeMiddleware, LoggerMiddleware, RequestCacheMiddleware } from "effect-app/middleware"
|
|
4
|
+
import { RpcContextMap, type RpcMiddleware } from "effect-app/rpc"
|
|
4
5
|
import { pretty } from "effect-app/utils"
|
|
6
|
+
import { type Rpc } from "effect/unstable/rpc"
|
|
5
7
|
import { logError, reportError } from "../../../errorReporter.js"
|
|
6
8
|
import { InfraLogger } from "../../../logger.js"
|
|
9
|
+
import { WithNsTransaction } from "../../../Store/SQL.js"
|
|
7
10
|
import { determineMethod, isCommand } from "../utils.js"
|
|
8
11
|
|
|
9
12
|
const logRequestError = logError("Request")
|
|
@@ -126,3 +129,43 @@ export const DefaultGenericMiddlewaresLive = Layer.mergeAll(
|
|
|
126
129
|
LoggerMiddlewareLive,
|
|
127
130
|
DevModeMiddlewareLive
|
|
128
131
|
)
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Config entry for `RequestContextMap` that controls per-RPC transaction wrapping.
|
|
135
|
+
* Defaults to `false` (no transaction). Set `requiresTransaction: true` on a route to enable.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* class RequestContextMap extends RpcContextMap.makeMap({
|
|
140
|
+
* requiresTransaction: requiresTransactionConfig,
|
|
141
|
+
* // ...
|
|
142
|
+
* }) {}
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export const requiresTransactionConfig = RpcContextMap.makeCustom()(Schema.Never, false as boolean)
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates the middleware Effect for SQL transaction wrapping.
|
|
149
|
+
* Requires `WithNsTransaction` service.
|
|
150
|
+
* Reads `requiresTransaction` from the RPC config; defaults to `false`.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* const SqlTransactionMiddlewareLive = Layer.effect(
|
|
155
|
+
* SqlTransactionMiddleware,
|
|
156
|
+
* makeSqlTransactionMiddleware(RequestContextMap)
|
|
157
|
+
* )
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export const makeSqlTransactionMiddleware = (
|
|
161
|
+
rcm: { getConfig: (rpc: Rpc.AnyWithProps) => { readonly requiresTransaction?: boolean } }
|
|
162
|
+
) =>
|
|
163
|
+
Effect.gen(function*() {
|
|
164
|
+
const withTx = yield* WithNsTransaction
|
|
165
|
+
const mw: RpcMiddleware.RpcMiddlewareV4<never, never, never> = (effect, { rpc }) => {
|
|
166
|
+
const { requiresTransaction } = rcm.getConfig(rpc)
|
|
167
|
+
if (requiresTransaction !== true) return effect
|
|
168
|
+
return withTx(effect)
|
|
169
|
+
}
|
|
170
|
+
return mw
|
|
171
|
+
})
|
package/src/api/routing.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
4
4
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
5
|
import { Config, Effect, Layer, type NonEmptyReadonlyArray, Predicate, S, type Scope } from "effect-app"
|
|
6
|
+
import { getMeta } from "effect-app/client"
|
|
6
7
|
import { type HttpHeaders } from "effect-app/http"
|
|
7
8
|
import { type GetEffectContext, type GetEffectError, type RpcContextMap } from "effect-app/rpc/RpcContextMap"
|
|
8
9
|
import { type TypeTestId } from "effect-app/TypeTest"
|
|
@@ -182,10 +183,9 @@ export const makeRouter = <
|
|
|
182
183
|
* if `check` is provided, the router will only be created if the effect succeeds with true
|
|
183
184
|
*/
|
|
184
185
|
function matchFor<
|
|
185
|
-
const ModuleName extends string,
|
|
186
186
|
const Resource extends Record<string, any>
|
|
187
187
|
>(
|
|
188
|
-
rsc: Resource
|
|
188
|
+
rsc: Resource,
|
|
189
189
|
options?: { check?: Effect.Effect<boolean> }
|
|
190
190
|
) {
|
|
191
191
|
type HandlerWithInputGen<
|
|
@@ -244,7 +244,7 @@ export const makeRouter = <
|
|
|
244
244
|
|
|
245
245
|
type AnyHandlers<Action extends AnyRequestModule> = HandlersRaw<Action> | HandlersDecoded<Action>
|
|
246
246
|
|
|
247
|
-
const
|
|
247
|
+
const meta = getMeta(rsc)
|
|
248
248
|
|
|
249
249
|
type RequestModules = FilterRequestModules<Resource>
|
|
250
250
|
const requestModules = typedKeysOf(rsc).reduce((acc, cur) => {
|
package/src/api/setupRequest.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { Effect, Layer, Tracer } from "effect-app"
|
|
1
|
+
import { Effect, Layer, Option, Tracer } from "effect-app"
|
|
2
2
|
import { NonEmptyString255 } from "effect-app/Schema"
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql"
|
|
3
4
|
import { LocaleRef, RequestContext, spanAttributes } from "../RequestContext.js"
|
|
4
5
|
import { ContextMapContainer } from "../Store/ContextMapContainer.js"
|
|
5
6
|
import { storeId } from "../Store/Memory.js"
|
|
6
7
|
|
|
8
|
+
const withSqlTransaction = <R, E, A>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
9
|
+
Effect.serviceOption(SqlClient.SqlClient).pipe(
|
|
10
|
+
Effect.flatMap(Option.match({
|
|
11
|
+
onNone: () => self,
|
|
12
|
+
onSome: (sql) => sql.withTransaction(self).pipe(Effect.orDie)
|
|
13
|
+
}))
|
|
14
|
+
)
|
|
15
|
+
|
|
7
16
|
export const getRequestContext = Effect
|
|
8
17
|
.all({
|
|
9
18
|
span: Effect.currentSpan.pipe(Effect.orDie),
|
|
@@ -43,16 +52,25 @@ const withRequestSpan = (name = "request", options?: Tracer.SpanOptions) => <R,
|
|
|
43
52
|
)
|
|
44
53
|
)
|
|
45
54
|
|
|
55
|
+
export interface SetupRequestOptions {
|
|
56
|
+
readonly withTransaction?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
export const setupRequestContextFromCurrent =
|
|
47
|
-
(name = "request", options?: Tracer.SpanOptions) => <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
60
|
+
(name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
48
61
|
self
|
|
49
62
|
.pipe(
|
|
63
|
+
options?.withTransaction === true ? withSqlTransaction : (_) => _,
|
|
50
64
|
withRequestSpan(name, options),
|
|
51
|
-
Effect.provide(ContextMapContainer.layer)
|
|
65
|
+
Effect.provide(ContextMapContainer.layer, { local: true })
|
|
52
66
|
)
|
|
53
67
|
|
|
54
68
|
// TODO: consider integrating Effect.withParentSpan
|
|
55
|
-
export function setupRequestContext<R, E, A>(
|
|
69
|
+
export function setupRequestContext<R, E, A>(
|
|
70
|
+
self: Effect.Effect<A, E, R>,
|
|
71
|
+
requestContext: RequestContext,
|
|
72
|
+
options?: SetupRequestOptions
|
|
73
|
+
) {
|
|
56
74
|
const layer = Layer.mergeAll(
|
|
57
75
|
ContextMapContainer.layer,
|
|
58
76
|
Layer.succeed(LocaleRef, requestContext.locale),
|
|
@@ -60,8 +78,9 @@ export function setupRequestContext<R, E, A>(self: Effect.Effect<A, E, R>, reque
|
|
|
60
78
|
)
|
|
61
79
|
return self
|
|
62
80
|
.pipe(
|
|
81
|
+
options?.withTransaction === true ? withSqlTransaction : (_) => _,
|
|
63
82
|
withRequestSpan(requestContext.name),
|
|
64
|
-
Effect.provide(layer)
|
|
83
|
+
Effect.provide(layer, { local: true })
|
|
65
84
|
)
|
|
66
85
|
}
|
|
67
86
|
|
|
@@ -69,7 +88,7 @@ export function setupRequestContextWithCustomSpan<R, E, A>(
|
|
|
69
88
|
self: Effect.Effect<A, E, R>,
|
|
70
89
|
requestContext: RequestContext,
|
|
71
90
|
name: string,
|
|
72
|
-
options?: Tracer.SpanOptions
|
|
91
|
+
options?: Tracer.SpanOptions & SetupRequestOptions
|
|
73
92
|
) {
|
|
74
93
|
const layer = Layer.mergeAll(
|
|
75
94
|
ContextMapContainer.layer,
|
|
@@ -78,7 +97,8 @@ export function setupRequestContextWithCustomSpan<R, E, A>(
|
|
|
78
97
|
)
|
|
79
98
|
return self
|
|
80
99
|
.pipe(
|
|
100
|
+
options?.withTransaction === true ? withSqlTransaction : (_) => _,
|
|
81
101
|
withRequestSpan(name, options),
|
|
82
|
-
Effect.provide(layer)
|
|
102
|
+
Effect.provide(layer, { local: true })
|
|
83
103
|
)
|
|
84
104
|
}
|