@effect-app/infra 2.0.1 → 2.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.
- package/CHANGELOG.md +12 -0
- package/_cjs/api/internal/RequestContextMiddleware.cjs +2 -3
- package/_cjs/api/internal/RequestContextMiddleware.cjs.map +1 -1
- package/_cjs/api/internal/events.cjs +2 -2
- package/_cjs/api/internal/events.cjs.map +1 -1
- package/_cjs/api/setupRequest.cjs +1 -1
- package/_cjs/api/setupRequest.cjs.map +1 -1
- package/_cjs/fileUtil.cjs +48 -0
- package/_cjs/fileUtil.cjs.map +1 -0
- package/_cjs/logger/shared.cjs +2 -2
- package/_cjs/logger/shared.cjs.map +1 -1
- package/_cjs/services/CUPS.cjs +118 -0
- package/_cjs/services/CUPS.cjs.map +1 -0
- package/_cjs/services/QueueMaker/SQLQueue.cjs +1 -1
- package/_cjs/services/QueueMaker/SQLQueue.cjs.map +1 -1
- package/_cjs/services/QueueMaker/memQueue.cjs +1 -1
- package/_cjs/services/QueueMaker/memQueue.cjs.map +1 -1
- package/_cjs/services/QueueMaker/sbqueue.cjs +1 -1
- package/_cjs/services/QueueMaker/sbqueue.cjs.map +1 -1
- package/_cjs/services/Store/Cosmos.cjs +1 -1
- package/_cjs/services/Store/Cosmos.cjs.map +1 -1
- package/_cjs/services/Store/Disk.cjs +1 -1
- package/_cjs/services/adapters/SQL/Model.cjs +500 -0
- package/_cjs/services/adapters/SQL/Model.cjs.map +1 -0
- package/_cjs/services/adapters/SQL.cjs +11 -0
- package/_cjs/services/adapters/SQL.cjs.map +1 -0
- package/_cjs/services/adapters/ServiceBus.cjs +76 -0
- package/_cjs/services/adapters/ServiceBus.cjs.map +1 -0
- package/_cjs/services/adapters/cosmos-client.cjs +18 -0
- package/_cjs/services/adapters/cosmos-client.cjs.map +1 -0
- package/_cjs/services/adapters/index.cjs +6 -0
- package/_cjs/services/adapters/index.cjs.map +1 -0
- package/_cjs/services/adapters/logger.cjs +9 -0
- package/_cjs/services/adapters/logger.cjs.map +1 -0
- package/_cjs/services/adapters/memQueue.cjs +31 -0
- package/_cjs/services/adapters/memQueue.cjs.map +1 -0
- package/_cjs/services/adapters/mongo-client.cjs +20 -0
- package/_cjs/services/adapters/mongo-client.cjs.map +1 -0
- package/_cjs/services/adapters/redis-client.cjs +83 -0
- package/_cjs/services/adapters/redis-client.cjs.map +1 -0
- package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
- package/dist/api/internal/RequestContextMiddleware.js +3 -4
- package/dist/api/internal/events.d.ts.map +1 -1
- package/dist/api/internal/events.js +3 -3
- package/dist/api/setupRequest.d.ts +1 -2
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +3 -3
- package/dist/fileUtil.d.ts +23 -0
- package/dist/fileUtil.d.ts.map +1 -0
- package/dist/fileUtil.js +41 -0
- package/dist/logger/shared.d.ts.map +1 -1
- package/dist/logger/shared.js +2 -2
- package/dist/services/CUPS.d.ts +26 -0
- package/dist/services/CUPS.d.ts.map +1 -0
- package/dist/services/CUPS.js +111 -0
- package/dist/services/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/services/QueueMaker/SQLQueue.js +2 -2
- package/dist/services/QueueMaker/memQueue.d.ts +1 -1
- package/dist/services/QueueMaker/memQueue.d.ts.map +1 -1
- package/dist/services/QueueMaker/memQueue.js +2 -2
- package/dist/services/QueueMaker/sbqueue.d.ts +3 -3
- package/dist/services/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/services/QueueMaker/sbqueue.js +2 -2
- package/dist/services/Store/Cosmos.d.ts.map +1 -1
- package/dist/services/Store/Cosmos.js +2 -2
- package/dist/services/Store/Disk.js +2 -2
- package/dist/services/adapters/SQL/Model.d.ts +538 -0
- package/dist/services/adapters/SQL/Model.d.ts.map +1 -0
- package/dist/services/adapters/SQL/Model.js +508 -0
- package/dist/services/adapters/SQL.d.ts +2 -0
- package/dist/services/adapters/SQL.d.ts.map +1 -0
- package/dist/services/adapters/SQL.js +2 -0
- package/dist/services/adapters/ServiceBus.d.ts +50 -0
- package/dist/services/adapters/ServiceBus.d.ts.map +1 -0
- package/dist/services/adapters/ServiceBus.js +73 -0
- package/dist/services/adapters/cosmos-client.d.ts +10 -0
- package/dist/services/adapters/cosmos-client.d.ts.map +1 -0
- package/dist/services/adapters/cosmos-client.js +8 -0
- package/dist/services/adapters/index.d.ts +2 -0
- package/dist/services/adapters/index.d.ts.map +1 -0
- package/dist/services/adapters/index.js +2 -0
- package/dist/services/adapters/logger.d.ts +8 -0
- package/dist/services/adapters/logger.d.ts.map +1 -0
- package/dist/services/adapters/logger.js +3 -0
- package/dist/services/adapters/memQueue.d.ts +34 -0
- package/dist/services/adapters/memQueue.d.ts.map +1 -0
- package/dist/services/adapters/memQueue.js +24 -0
- package/dist/services/adapters/mongo-client.d.ts +10 -0
- package/dist/services/adapters/mongo-client.d.ts.map +1 -0
- package/dist/services/adapters/mongo-client.js +12 -0
- package/dist/services/adapters/redis-client.d.ts +29 -0
- package/dist/services/adapters/redis-client.d.ts.map +1 -0
- package/dist/services/adapters/redis-client.js +93 -0
- package/package.json +128 -12
- package/src/api/internal/RequestContextMiddleware.ts +2 -3
- package/src/api/internal/events.ts +2 -2
- package/src/api/setupRequest.ts +2 -3
- package/src/fileUtil.ts +85 -0
- package/src/logger/shared.ts +2 -3
- package/src/services/CUPS.ts +151 -0
- package/src/services/QueueMaker/SQLQueue.ts +1 -1
- package/src/services/QueueMaker/memQueue.ts +1 -1
- package/src/services/QueueMaker/sbqueue.ts +7 -7
- package/src/services/Store/Cosmos.ts +1 -1
- package/src/services/Store/Disk.ts +1 -1
- package/src/services/adapters/SQL/Model.ts +939 -0
- package/src/services/adapters/SQL.ts +1 -0
- package/src/services/adapters/ServiceBus.ts +140 -0
- package/src/services/adapters/cosmos-client.ts +16 -0
- package/src/services/adapters/index.ts +0 -0
- package/src/services/adapters/logger.ts +3 -0
- package/src/services/adapters/memQueue.ts +26 -0
- package/src/services/adapters/mongo-client.ts +23 -0
- package/src/services/adapters/redis-client.ts +123 -0
- package/tsconfig.src.json +0 -3
- package/src/services/Store/Redis.ts.bak +0 -88
- package/src/services/simpledb/cosmosdb.ts.bak +0 -149
- package/src/services/simpledb/diskdb.ts.bak +0 -165
- package/src/services/simpledb/index.ts.bak +0 -6
- package/src/services/simpledb/memdb.ts.bak +0 -78
- package/src/services/simpledb/mongodb.ts.bak +0 -107
- package/src/services/simpledb/redisdb.ts.bak +0 -202
- package/src/services/simpledb/shared.ts.bak +0 -117
- package/src/services/simpledb/simpledb.ts.bak +0 -121
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Model from "./SQL/Model.js"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OperationOptionsBase,
|
|
3
|
+
ProcessErrorArgs,
|
|
4
|
+
ServiceBusMessage,
|
|
5
|
+
ServiceBusMessageBatch,
|
|
6
|
+
ServiceBusReceivedMessage,
|
|
7
|
+
ServiceBusReceiver,
|
|
8
|
+
ServiceBusSender
|
|
9
|
+
} from "@azure/service-bus"
|
|
10
|
+
import { ServiceBusClient } from "@azure/service-bus"
|
|
11
|
+
import type { Scope } from "effect-app"
|
|
12
|
+
import { Cause, Context, Effect, Exit, FiberSet, Layer } from "effect-app"
|
|
13
|
+
import { InfraLogger } from "./logger.js"
|
|
14
|
+
|
|
15
|
+
function makeClient(url: string) {
|
|
16
|
+
return Effect.acquireRelease(
|
|
17
|
+
Effect.sync(() => new ServiceBusClient(url)),
|
|
18
|
+
(client) => Effect.promise(() => client.close())
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const Client = Context.GenericTag<ServiceBusClient>("@services/Client")
|
|
23
|
+
export const LiveServiceBusClient = (url: string) => Layer.scoped(Client)(makeClient(url))
|
|
24
|
+
|
|
25
|
+
function makeSender(queueName: string) {
|
|
26
|
+
return Effect.gen(function*() {
|
|
27
|
+
const serviceBusClient = yield* Client
|
|
28
|
+
|
|
29
|
+
return yield* Effect.acquireRelease(
|
|
30
|
+
Effect.sync(() => serviceBusClient.createSender(queueName)),
|
|
31
|
+
(subscription) => Effect.promise(() => subscription.close())
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
export const Sender = Context.GenericTag<ServiceBusSender>("@services/Sender")
|
|
36
|
+
|
|
37
|
+
export function LiveSender(queueName: string) {
|
|
38
|
+
return Layer
|
|
39
|
+
.scoped(Sender, makeSender(queueName))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeReceiver(queueName: string, waitTillEmpty: Effect<void>, sessionId?: string) {
|
|
43
|
+
return Effect.gen(function*() {
|
|
44
|
+
const serviceBusClient = yield* Client
|
|
45
|
+
|
|
46
|
+
return yield* Effect.acquireRelease(
|
|
47
|
+
sessionId
|
|
48
|
+
? Effect.promise(() => serviceBusClient.acceptSession(queueName, sessionId))
|
|
49
|
+
: Effect.sync(() => serviceBusClient.createReceiver(queueName)),
|
|
50
|
+
(r) => waitTillEmpty.pipe(Effect.andThen(Effect.promise(() => r.close())))
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ServiceBusReceiverFactory extends Context.TagId(
|
|
56
|
+
"ServiceBusReceiverFactory"
|
|
57
|
+
)<ServiceBusReceiverFactory, {
|
|
58
|
+
make: (waitTillEmpty: Effect<void>) => Effect<ServiceBusReceiver, never, Scope>
|
|
59
|
+
makeSession: (sessionId: string, waitTillEmpty: Effect<void>) => Effect<ServiceBusReceiver, never, Scope>
|
|
60
|
+
}>() {
|
|
61
|
+
static readonly Live = (queueName: string) =>
|
|
62
|
+
this.toLayer(Client.pipe(Effect.andThen((cl) => ({
|
|
63
|
+
make: (waitTillEmpty: Effect<void>) =>
|
|
64
|
+
makeReceiver(queueName, waitTillEmpty).pipe(Effect.provideService(Client, cl)),
|
|
65
|
+
makeSession: (sessionId: string, waitTillEmpty: Effect<void>) =>
|
|
66
|
+
makeReceiver(queueName, waitTillEmpty, sessionId).pipe(Effect.provideService(Client, cl))
|
|
67
|
+
}))))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sendMessages(
|
|
71
|
+
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
72
|
+
options?: OperationOptionsBase
|
|
73
|
+
) {
|
|
74
|
+
return Effect.gen(function*() {
|
|
75
|
+
const s = yield* Sender
|
|
76
|
+
return yield* Effect.promise(() => s.sendMessages(messages, options))
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function subscribe<RMsg, RErr>(hndlr: MessageHandlers<RMsg, RErr>, sessionId?: string) {
|
|
81
|
+
return Effect.gen(function*() {
|
|
82
|
+
const rf = yield* ServiceBusReceiverFactory
|
|
83
|
+
const fs = yield* FiberSet.make()
|
|
84
|
+
const fr = yield* FiberSet.runtime(fs)<RMsg | RErr>()
|
|
85
|
+
const wait = Effect.gen(function*() {
|
|
86
|
+
if ((yield* FiberSet.size(fs)) > 0) {
|
|
87
|
+
yield* InfraLogger.logDebug("Waiting ServiceBusFiberSet to be empty: " + (yield* FiberSet.size(fs)))
|
|
88
|
+
}
|
|
89
|
+
while ((yield* FiberSet.size(fs)) > 0) yield* Effect.sleep("250 millis")
|
|
90
|
+
})
|
|
91
|
+
const r = yield* sessionId
|
|
92
|
+
? rf.makeSession(
|
|
93
|
+
sessionId,
|
|
94
|
+
wait
|
|
95
|
+
)
|
|
96
|
+
: rf.make(wait)
|
|
97
|
+
|
|
98
|
+
const runEffect = <E>(effect: Effect<void, E, RMsg | RErr>) =>
|
|
99
|
+
new Promise<void>((resolve, reject) =>
|
|
100
|
+
fr(effect)
|
|
101
|
+
.addObserver((exit) => {
|
|
102
|
+
if (Exit.isSuccess(exit)) {
|
|
103
|
+
resolve(exit.value)
|
|
104
|
+
} else {
|
|
105
|
+
reject(Cause.pretty(exit.cause, { renderErrorCause: true }))
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
yield* Effect.acquireRelease(
|
|
110
|
+
Effect.sync(() =>
|
|
111
|
+
r.subscribe({
|
|
112
|
+
processError: (err) =>
|
|
113
|
+
runEffect(
|
|
114
|
+
hndlr
|
|
115
|
+
.processError(err)
|
|
116
|
+
.pipe(Effect.catchAllCause((cause) => Effect.logError("ServiceBus Error", cause)))
|
|
117
|
+
),
|
|
118
|
+
processMessage: (msg) => runEffect(hndlr.processMessage(msg))
|
|
119
|
+
// DO NOT CATCH ERRORS here as they should return to the queue!
|
|
120
|
+
})
|
|
121
|
+
),
|
|
122
|
+
(subscription) => Effect.promise(() => subscription.close())
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface MessageHandlers<RMsg, RErr> {
|
|
128
|
+
/**
|
|
129
|
+
* Handler that processes messages from service bus.
|
|
130
|
+
*
|
|
131
|
+
* @param message - A message received from Service Bus.
|
|
132
|
+
*/
|
|
133
|
+
processMessage(message: ServiceBusReceivedMessage): Effect<void, never, RMsg>
|
|
134
|
+
/**
|
|
135
|
+
* Handler that processes errors that occur during receiving.
|
|
136
|
+
* @param args - The error and additional context to indicate where
|
|
137
|
+
* the error originated.
|
|
138
|
+
*/
|
|
139
|
+
processError(args: ProcessErrorArgs): Effect<void, never, RErr>
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CosmosClient as ComosClient_ } from "@azure/cosmos"
|
|
2
|
+
import { Context, Effect, Layer } from "effect-app"
|
|
3
|
+
|
|
4
|
+
const withClient = (url: string) => Effect.sync(() => new ComosClient_(url))
|
|
5
|
+
|
|
6
|
+
export const makeCosmosClient = (url: string, dbName: string) =>
|
|
7
|
+
Effect.map(withClient(url), (x) => ({ db: x.database(dbName) }))
|
|
8
|
+
|
|
9
|
+
export interface CosmosClient extends Effect.Success<ReturnType<typeof makeCosmosClient>> {}
|
|
10
|
+
|
|
11
|
+
export const CosmosClient = Context.GenericTag<CosmosClient>("@services/CosmosClient")
|
|
12
|
+
|
|
13
|
+
export const db = Effect.map(CosmosClient, (_) => _.db)
|
|
14
|
+
|
|
15
|
+
export const CosmosClientLayer = (cosmosUrl: string, dbName: string) =>
|
|
16
|
+
Layer.effect(CosmosClient, makeCosmosClient(cosmosUrl, dbName))
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Context, Effect, type Queue } from "effect-app"
|
|
2
|
+
import * as Q from "effect/Queue"
|
|
3
|
+
|
|
4
|
+
const make = Effect
|
|
5
|
+
.gen(function*() {
|
|
6
|
+
const store = yield* Effect.sync(() => new Map<string, Queue.Queue<string>>())
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
getOrCreateQueue: (k: string) =>
|
|
10
|
+
Effect.gen(function*() {
|
|
11
|
+
const q = store.get(k)
|
|
12
|
+
if (q) return q
|
|
13
|
+
const newQ = yield* Q.unbounded<string>()
|
|
14
|
+
store.set(k, newQ)
|
|
15
|
+
return newQ
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @tsplus type MemQueue
|
|
22
|
+
* @tsplus companion MemQueue.Ops
|
|
23
|
+
*/
|
|
24
|
+
export class MemQueue extends Context.TagMakeId("effect-app/MemQueue", make)<MemQueue>() {
|
|
25
|
+
static readonly Live = this.toLayer()
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect-app"
|
|
2
|
+
import { MongoClient as MongoClient_ } from "mongodb"
|
|
3
|
+
|
|
4
|
+
// TODO: we should probably share a single client...
|
|
5
|
+
|
|
6
|
+
const withClient = (url: string) =>
|
|
7
|
+
Effect.acquireRelease(
|
|
8
|
+
Effect
|
|
9
|
+
.promise(() => {
|
|
10
|
+
const client = new MongoClient_(url)
|
|
11
|
+
return client.connect()
|
|
12
|
+
}),
|
|
13
|
+
(cl) => Effect.promise(() => cl.close()).pipe(Effect.uninterruptible, Effect.orDie)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const makeMongoClient = (url: string, dbName?: string) => Effect.map(withClient(url), (x) => ({ db: x.db(dbName) }))
|
|
17
|
+
|
|
18
|
+
export interface MongoClient extends Effect.Success<ReturnType<typeof makeMongoClient>> {}
|
|
19
|
+
|
|
20
|
+
export const MongoClient = Context.GenericTag<MongoClient>("@services/MongoClient")
|
|
21
|
+
|
|
22
|
+
export const MongoClientLive = (mongoUrl: string, dbName?: string) =>
|
|
23
|
+
Layer.scoped(MongoClient, makeMongoClient(mongoUrl, dbName))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer, Option } from "effect-app"
|
|
2
|
+
import type { RedisClient as Client } from "redis"
|
|
3
|
+
import Redlock from "redlock"
|
|
4
|
+
|
|
5
|
+
export class ConnectionException extends Data.TaggedError("ConnectionException")<{ cause: Error; message: string }> {
|
|
6
|
+
constructor(cause: Error) {
|
|
7
|
+
super({ message: "A connection error ocurred", cause })
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const makeRedisClient = (makeClient: () => Client) =>
|
|
12
|
+
Effect.acquireRelease(
|
|
13
|
+
Effect
|
|
14
|
+
.sync(() => {
|
|
15
|
+
const client = createClient(makeClient)
|
|
16
|
+
const lock = new Redlock([client])
|
|
17
|
+
|
|
18
|
+
function get(key: string) {
|
|
19
|
+
return Effect
|
|
20
|
+
.async<Option<string>, ConnectionException>((res) => {
|
|
21
|
+
client.get(key, (err, v) =>
|
|
22
|
+
err
|
|
23
|
+
? res(new ConnectionException(err))
|
|
24
|
+
: res(Effect.sync(() => Option.fromNullable(v))))
|
|
25
|
+
})
|
|
26
|
+
.pipe(Effect.uninterruptible)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function set(key: string, val: string) {
|
|
30
|
+
return Effect
|
|
31
|
+
.async<void, ConnectionException>((res) => {
|
|
32
|
+
client.set(key, val, (err) =>
|
|
33
|
+
err
|
|
34
|
+
? res(new ConnectionException(err))
|
|
35
|
+
: res(Effect.sync(() => void 0)))
|
|
36
|
+
})
|
|
37
|
+
.pipe(Effect.uninterruptible)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hset(key: string, field: string, value: string) {
|
|
41
|
+
return Effect
|
|
42
|
+
.async<void, ConnectionException>((res) => {
|
|
43
|
+
client.hset(key, field, value, (err) =>
|
|
44
|
+
err
|
|
45
|
+
? res(new ConnectionException(err))
|
|
46
|
+
: res(Effect.sync(() => void 0)))
|
|
47
|
+
})
|
|
48
|
+
.pipe(Effect.uninterruptible)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hget(key: string, field: string) {
|
|
52
|
+
return Effect
|
|
53
|
+
.async<Option<string>, ConnectionException>((res) => {
|
|
54
|
+
client.hget(key, field, (err, v) =>
|
|
55
|
+
err
|
|
56
|
+
? res(new ConnectionException(err))
|
|
57
|
+
: res(Effect.sync(() => Option.fromNullable(v))))
|
|
58
|
+
})
|
|
59
|
+
.pipe(Effect.uninterruptible)
|
|
60
|
+
}
|
|
61
|
+
function hmgetAll(key: string) {
|
|
62
|
+
return Effect
|
|
63
|
+
.async<Option<{ [key: string]: string }>, ConnectionException>(
|
|
64
|
+
(res) => {
|
|
65
|
+
client.hgetall(key, (err, v) =>
|
|
66
|
+
err
|
|
67
|
+
? res(new ConnectionException(err))
|
|
68
|
+
: res(Effect.sync(() => Option.fromNullable(v))))
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
.pipe(Effect.uninterruptible)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
client,
|
|
76
|
+
lock,
|
|
77
|
+
|
|
78
|
+
get,
|
|
79
|
+
hget,
|
|
80
|
+
hset,
|
|
81
|
+
hmgetAll,
|
|
82
|
+
set
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
(cl) =>
|
|
86
|
+
Effect
|
|
87
|
+
.async<void, Error>((res) => {
|
|
88
|
+
cl.client.quit((err) => res(err ? Effect.fail(err) : Effect.void))
|
|
89
|
+
})
|
|
90
|
+
.pipe(Effect.uninterruptible, Effect.orDie)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
export interface RedisClient extends Effect.Success<ReturnType<typeof makeRedisClient>> {}
|
|
94
|
+
|
|
95
|
+
export const RedisClient = Context.GenericTag<RedisClient>("@services/RedisClient")
|
|
96
|
+
|
|
97
|
+
export const RedisClientLayer = (storageUrl: string) =>
|
|
98
|
+
Layer.scoped(RedisClient, makeRedisClient(makeRedis(storageUrl)))
|
|
99
|
+
|
|
100
|
+
function createClient(makeClient: () => Client) {
|
|
101
|
+
const client = makeClient()
|
|
102
|
+
client.on("error", (error) => {
|
|
103
|
+
console.error(error)
|
|
104
|
+
})
|
|
105
|
+
return client
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function makeRedis(storageUrl: string) {
|
|
109
|
+
const url = new URL(storageUrl)
|
|
110
|
+
const hostname = url.hostname
|
|
111
|
+
const password = url.password
|
|
112
|
+
return () =>
|
|
113
|
+
createClient(
|
|
114
|
+
storageUrl === "redis://"
|
|
115
|
+
? ({
|
|
116
|
+
host: hostname,
|
|
117
|
+
port: 6380,
|
|
118
|
+
auth_pass: password,
|
|
119
|
+
tls: { servername: hostname }
|
|
120
|
+
} as any)
|
|
121
|
+
: (storageUrl as any)
|
|
122
|
+
)
|
|
123
|
+
}
|
package/tsconfig.src.json
CHANGED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { RedisClient, RedisClientLayer } from "@effect-app/infra-adapters/redis-client"
|
|
3
|
-
import { Effect, Layer, Option, ReadonlyArray, Secret } from "effect-app"
|
|
4
|
-
import type { NonEmptyArray, NonEmptyReadonlyArray } from "effect-app"
|
|
5
|
-
import { NotFoundError } from "../../errors.js"
|
|
6
|
-
import { memFilter } from "./Memory.js"
|
|
7
|
-
import type { PersistenceModelType, StorageConfig, Store, StoreConfig } from "./service.js"
|
|
8
|
-
import { StoreMaker } from "./service.js"
|
|
9
|
-
import { makeETag, makeUpdateETag } from "./utils.js"
|
|
10
|
-
|
|
11
|
-
function makeRedisStore({ prefix }: StorageConfig) {
|
|
12
|
-
return Effect.gen(function*($) {
|
|
13
|
-
const redis = yield* $(RedisClient)
|
|
14
|
-
return {
|
|
15
|
-
make: <Id extends string, PM extends PersistenceModelType<Id>, R = never, E = never>(
|
|
16
|
-
name: string,
|
|
17
|
-
seed?: Effect<Iterable<PM>, E, R>,
|
|
18
|
-
_config?: StoreConfig<PM>
|
|
19
|
-
) =>
|
|
20
|
-
Effect.gen(function*($) {
|
|
21
|
-
const updateETag = makeUpdateETag(name)
|
|
22
|
-
// Very naive implementation of course.
|
|
23
|
-
const key = `${prefix}${name}`
|
|
24
|
-
const current = yield* $(redis.get(key).orDie)
|
|
25
|
-
if (!current.isSome()) {
|
|
26
|
-
const m = yield* $(seed ?? Effect.sync(() => []))
|
|
27
|
-
yield* $(
|
|
28
|
-
redis
|
|
29
|
-
.set(key, JSON.stringify({ data: [...m].map((e) => makeETag(e)) }))
|
|
30
|
-
.orDie
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
const get = redis
|
|
34
|
-
.get(key)
|
|
35
|
-
.flatMap((x) => x.mapError(() => new NotFoundError<"data">({ type: "data", id: "" })))
|
|
36
|
-
.orDie
|
|
37
|
-
.map((x) => JSON.parse(x) as { data: readonly PM[] })
|
|
38
|
-
.map((_) => _.data)
|
|
39
|
-
|
|
40
|
-
const set = (i: ReadonlyMap<Id, PM>) => redis.set(key, JSON.stringify({ data: [...i.values()] })).orDie
|
|
41
|
-
|
|
42
|
-
const sem = Effect.unsafeMakeSemaphore(1)
|
|
43
|
-
const withPermit = sem.withPermits(1)
|
|
44
|
-
|
|
45
|
-
const asMap = get.map((x) => new Map(x.map((x) => [x.id, x] as const)))
|
|
46
|
-
const all = get.map(Array.fromIterable)
|
|
47
|
-
const batchSet = (items: NonEmptyReadonlyArray<PM>) =>
|
|
48
|
-
Effect
|
|
49
|
-
.forEach(items, (e) => s.find(e.id).flatMap((current) => updateETag(e, current)))
|
|
50
|
-
.tap((items) =>
|
|
51
|
-
asMap
|
|
52
|
-
.map((m) => {
|
|
53
|
-
const mut = m
|
|
54
|
-
items.forEach((e) => mut.set(e.id, e))
|
|
55
|
-
return mut
|
|
56
|
-
})
|
|
57
|
-
.flatMap(set)
|
|
58
|
-
)
|
|
59
|
-
.map((_) => _ as NonEmptyArray<PM>)
|
|
60
|
-
.pipe(withPermit)
|
|
61
|
-
const s: Store<PM, Id> = {
|
|
62
|
-
all,
|
|
63
|
-
filter: (f) => all.map(memFilter(f)),
|
|
64
|
-
find: (id) => asMap.map((_) => Option.fromNullable(_.get(id))),
|
|
65
|
-
set: (e) =>
|
|
66
|
-
s
|
|
67
|
-
.find(e.id)
|
|
68
|
-
.flatMap((current) => updateETag(e, current))
|
|
69
|
-
.tap((e) => asMap.map((_) => new Map([..._, [e.id, e]])).flatMap(set))
|
|
70
|
-
.pipe(withPermit),
|
|
71
|
-
batchSet,
|
|
72
|
-
bulkSet: batchSet,
|
|
73
|
-
remove: (e: PM) =>
|
|
74
|
-
asMap
|
|
75
|
-
.map((_) => new Map([..._].filter(([_]) => _ !== e.id)))
|
|
76
|
-
.flatMap(set)
|
|
77
|
-
.pipe(withPermit)
|
|
78
|
-
}
|
|
79
|
-
return s
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
export function RedisStoreLayer(cfg: StorageConfig) {
|
|
85
|
-
return StoreMaker
|
|
86
|
-
.toLayer(makeRedisStore(cfg))
|
|
87
|
-
.pipe(Layer.provide(RedisClientLayer(Secret.value(cfg.url))))
|
|
88
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { IndexingPolicy } from "@azure/cosmos"
|
|
2
|
-
import { typedKeysOf } from "effect-app/utils"
|
|
3
|
-
|
|
4
|
-
import * as Cosmos from "@effect-app/infra-adapters/cosmos-client"
|
|
5
|
-
import { Data, Effect, Option } from "effect-app"
|
|
6
|
-
import type { CachedRecord, DBRecord } from "./shared.js"
|
|
7
|
-
import { OptimisticLockException } from "./shared.js"
|
|
8
|
-
import * as simpledb from "./simpledb.js"
|
|
9
|
-
import type { Version } from "./simpledb.js"
|
|
10
|
-
|
|
11
|
-
class CosmosDbOperationError extends Data.TaggedError("CosmosDbOperationError")<{ message: string }> {
|
|
12
|
-
constructor(message: string) {
|
|
13
|
-
super({ message })
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const setup = (type: string, indexingPolicy: IndexingPolicy) =>
|
|
18
|
-
Cosmos.CosmosClient.tap(({ db }) =>
|
|
19
|
-
Effect.tryPromise(() =>
|
|
20
|
-
db
|
|
21
|
-
.containers
|
|
22
|
-
.create({ id: type, indexingPolicy })
|
|
23
|
-
.catch((err) => console.warn(err))
|
|
24
|
-
)
|
|
25
|
-
)
|
|
26
|
-
// TODO: Error if current indexingPolicy does not match
|
|
27
|
-
// Effect.flatMap((db) => Effect.tryPromise(() => db.container(type).(indexes)))
|
|
28
|
-
export function createContext<TKey extends string, EA, A extends DBRecord<TKey>>() {
|
|
29
|
-
return <REncode, RDecode, EDecode>(
|
|
30
|
-
type: string,
|
|
31
|
-
encode: (record: A) => Effect<EA, never, REncode>,
|
|
32
|
-
decode: (d: EA) => Effect<A, EDecode, RDecode>,
|
|
33
|
-
// schemaVersion: string,
|
|
34
|
-
indexes: IndexingPolicy
|
|
35
|
-
) => {
|
|
36
|
-
return setup(type, indexes).map(() => ({
|
|
37
|
-
find: simpledb.find(find, decode, type),
|
|
38
|
-
findBy,
|
|
39
|
-
save: simpledb.storeDirectly(store, type)
|
|
40
|
-
}))
|
|
41
|
-
|
|
42
|
-
function find(id: string) {
|
|
43
|
-
return Cosmos
|
|
44
|
-
.CosmosClient
|
|
45
|
-
.flatMap(({ db }) => Effect.tryPromise(() => db.container(type).item(id).read<{ data: EA }>()))
|
|
46
|
-
.map((i) => Option.fromNullable(i.resource))
|
|
47
|
-
.map(
|
|
48
|
-
(_) =>
|
|
49
|
-
_.map(
|
|
50
|
-
({ _etag, data }) => ({ version: _etag, data } as CachedRecord<EA>)
|
|
51
|
-
)
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function findBy(parameters: Record<string, string>) {
|
|
56
|
-
return Cosmos
|
|
57
|
-
.CosmosClient
|
|
58
|
-
.flatMap(({ db }) =>
|
|
59
|
-
Effect.tryPromise(() =>
|
|
60
|
-
db
|
|
61
|
-
.container(type)
|
|
62
|
-
.items
|
|
63
|
-
.query({
|
|
64
|
-
query: `
|
|
65
|
-
SELECT TOP 1 ${type}.id
|
|
66
|
-
FROM ${type} i
|
|
67
|
-
WHERE (
|
|
68
|
-
${
|
|
69
|
-
typedKeysOf(parameters)
|
|
70
|
-
.map((k) => `i.${k} = @${k}`)
|
|
71
|
-
.join(" and ")
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
`,
|
|
75
|
-
parameters: typedKeysOf(parameters).map((p) => ({
|
|
76
|
-
name: `@${p}`,
|
|
77
|
-
value: parameters[p]!
|
|
78
|
-
}))
|
|
79
|
-
})
|
|
80
|
-
.fetchAll()
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
.map((x) => x.resources.head)
|
|
84
|
-
.map((_) => _.map((_) => _.id))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function store(record: A, currentVersion: Option<Version>) {
|
|
88
|
-
return Effect.gen(function*($) {
|
|
89
|
-
const version = "_etag" // we get this from the etag anyway.
|
|
90
|
-
|
|
91
|
-
const { db } = yield* $(Cosmos.CosmosClient)
|
|
92
|
-
const data = yield* $(encode(record))
|
|
93
|
-
|
|
94
|
-
yield* $(
|
|
95
|
-
currentVersion.match(
|
|
96
|
-
{
|
|
97
|
-
onNone: () =>
|
|
98
|
-
Effect
|
|
99
|
-
.tryPromise(() =>
|
|
100
|
-
db.container(type).items.create({
|
|
101
|
-
id: record.id,
|
|
102
|
-
timestamp: new Date(),
|
|
103
|
-
data
|
|
104
|
-
})
|
|
105
|
-
)
|
|
106
|
-
.asUnit
|
|
107
|
-
.orDie,
|
|
108
|
-
onSome: (currentVersion) =>
|
|
109
|
-
Effect
|
|
110
|
-
.tryPromise(() =>
|
|
111
|
-
db
|
|
112
|
-
.container(type)
|
|
113
|
-
.item(record.id)
|
|
114
|
-
.replace(
|
|
115
|
-
{
|
|
116
|
-
id: record.id,
|
|
117
|
-
timestamp: new Date(),
|
|
118
|
-
data
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
accessCondition: {
|
|
122
|
-
type: "IfMatch",
|
|
123
|
-
condition: currentVersion
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
)
|
|
127
|
-
)
|
|
128
|
-
.orDie
|
|
129
|
-
.flatMap((x) => {
|
|
130
|
-
if (x.statusCode === 412) {
|
|
131
|
-
return new OptimisticLockException(type, record.id)
|
|
132
|
-
}
|
|
133
|
-
if (x.statusCode > 299 || x.statusCode < 200) {
|
|
134
|
-
return Effect.die(
|
|
135
|
-
new CosmosDbOperationError(
|
|
136
|
-
"not able to update record: " + x.statusCode
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
return Effect.unit
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
)
|
|
144
|
-
)
|
|
145
|
-
return { version, data: record } as CachedRecord<A>
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|