@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/_cjs/api/internal/RequestContextMiddleware.cjs +2 -3
  3. package/_cjs/api/internal/RequestContextMiddleware.cjs.map +1 -1
  4. package/_cjs/api/internal/events.cjs +2 -2
  5. package/_cjs/api/internal/events.cjs.map +1 -1
  6. package/_cjs/api/setupRequest.cjs +1 -1
  7. package/_cjs/api/setupRequest.cjs.map +1 -1
  8. package/_cjs/fileUtil.cjs +48 -0
  9. package/_cjs/fileUtil.cjs.map +1 -0
  10. package/_cjs/logger/shared.cjs +2 -2
  11. package/_cjs/logger/shared.cjs.map +1 -1
  12. package/_cjs/services/CUPS.cjs +118 -0
  13. package/_cjs/services/CUPS.cjs.map +1 -0
  14. package/_cjs/services/QueueMaker/SQLQueue.cjs +1 -1
  15. package/_cjs/services/QueueMaker/SQLQueue.cjs.map +1 -1
  16. package/_cjs/services/QueueMaker/memQueue.cjs +1 -1
  17. package/_cjs/services/QueueMaker/memQueue.cjs.map +1 -1
  18. package/_cjs/services/QueueMaker/sbqueue.cjs +1 -1
  19. package/_cjs/services/QueueMaker/sbqueue.cjs.map +1 -1
  20. package/_cjs/services/Store/Cosmos.cjs +1 -1
  21. package/_cjs/services/Store/Cosmos.cjs.map +1 -1
  22. package/_cjs/services/Store/Disk.cjs +1 -1
  23. package/_cjs/services/adapters/SQL/Model.cjs +500 -0
  24. package/_cjs/services/adapters/SQL/Model.cjs.map +1 -0
  25. package/_cjs/services/adapters/SQL.cjs +11 -0
  26. package/_cjs/services/adapters/SQL.cjs.map +1 -0
  27. package/_cjs/services/adapters/ServiceBus.cjs +76 -0
  28. package/_cjs/services/adapters/ServiceBus.cjs.map +1 -0
  29. package/_cjs/services/adapters/cosmos-client.cjs +18 -0
  30. package/_cjs/services/adapters/cosmos-client.cjs.map +1 -0
  31. package/_cjs/services/adapters/index.cjs +6 -0
  32. package/_cjs/services/adapters/index.cjs.map +1 -0
  33. package/_cjs/services/adapters/logger.cjs +9 -0
  34. package/_cjs/services/adapters/logger.cjs.map +1 -0
  35. package/_cjs/services/adapters/memQueue.cjs +31 -0
  36. package/_cjs/services/adapters/memQueue.cjs.map +1 -0
  37. package/_cjs/services/adapters/mongo-client.cjs +20 -0
  38. package/_cjs/services/adapters/mongo-client.cjs.map +1 -0
  39. package/_cjs/services/adapters/redis-client.cjs +83 -0
  40. package/_cjs/services/adapters/redis-client.cjs.map +1 -0
  41. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  42. package/dist/api/internal/RequestContextMiddleware.js +3 -4
  43. package/dist/api/internal/events.d.ts.map +1 -1
  44. package/dist/api/internal/events.js +3 -3
  45. package/dist/api/setupRequest.d.ts +1 -2
  46. package/dist/api/setupRequest.d.ts.map +1 -1
  47. package/dist/api/setupRequest.js +3 -3
  48. package/dist/fileUtil.d.ts +23 -0
  49. package/dist/fileUtil.d.ts.map +1 -0
  50. package/dist/fileUtil.js +41 -0
  51. package/dist/logger/shared.d.ts.map +1 -1
  52. package/dist/logger/shared.js +2 -2
  53. package/dist/services/CUPS.d.ts +26 -0
  54. package/dist/services/CUPS.d.ts.map +1 -0
  55. package/dist/services/CUPS.js +111 -0
  56. package/dist/services/QueueMaker/SQLQueue.d.ts.map +1 -1
  57. package/dist/services/QueueMaker/SQLQueue.js +2 -2
  58. package/dist/services/QueueMaker/memQueue.d.ts +1 -1
  59. package/dist/services/QueueMaker/memQueue.d.ts.map +1 -1
  60. package/dist/services/QueueMaker/memQueue.js +2 -2
  61. package/dist/services/QueueMaker/sbqueue.d.ts +3 -3
  62. package/dist/services/QueueMaker/sbqueue.d.ts.map +1 -1
  63. package/dist/services/QueueMaker/sbqueue.js +2 -2
  64. package/dist/services/Store/Cosmos.d.ts.map +1 -1
  65. package/dist/services/Store/Cosmos.js +2 -2
  66. package/dist/services/Store/Disk.js +2 -2
  67. package/dist/services/adapters/SQL/Model.d.ts +538 -0
  68. package/dist/services/adapters/SQL/Model.d.ts.map +1 -0
  69. package/dist/services/adapters/SQL/Model.js +508 -0
  70. package/dist/services/adapters/SQL.d.ts +2 -0
  71. package/dist/services/adapters/SQL.d.ts.map +1 -0
  72. package/dist/services/adapters/SQL.js +2 -0
  73. package/dist/services/adapters/ServiceBus.d.ts +50 -0
  74. package/dist/services/adapters/ServiceBus.d.ts.map +1 -0
  75. package/dist/services/adapters/ServiceBus.js +73 -0
  76. package/dist/services/adapters/cosmos-client.d.ts +10 -0
  77. package/dist/services/adapters/cosmos-client.d.ts.map +1 -0
  78. package/dist/services/adapters/cosmos-client.js +8 -0
  79. package/dist/services/adapters/index.d.ts +2 -0
  80. package/dist/services/adapters/index.d.ts.map +1 -0
  81. package/dist/services/adapters/index.js +2 -0
  82. package/dist/services/adapters/logger.d.ts +8 -0
  83. package/dist/services/adapters/logger.d.ts.map +1 -0
  84. package/dist/services/adapters/logger.js +3 -0
  85. package/dist/services/adapters/memQueue.d.ts +34 -0
  86. package/dist/services/adapters/memQueue.d.ts.map +1 -0
  87. package/dist/services/adapters/memQueue.js +24 -0
  88. package/dist/services/adapters/mongo-client.d.ts +10 -0
  89. package/dist/services/adapters/mongo-client.d.ts.map +1 -0
  90. package/dist/services/adapters/mongo-client.js +12 -0
  91. package/dist/services/adapters/redis-client.d.ts +29 -0
  92. package/dist/services/adapters/redis-client.d.ts.map +1 -0
  93. package/dist/services/adapters/redis-client.js +93 -0
  94. package/package.json +128 -12
  95. package/src/api/internal/RequestContextMiddleware.ts +2 -3
  96. package/src/api/internal/events.ts +2 -2
  97. package/src/api/setupRequest.ts +2 -3
  98. package/src/fileUtil.ts +85 -0
  99. package/src/logger/shared.ts +2 -3
  100. package/src/services/CUPS.ts +151 -0
  101. package/src/services/QueueMaker/SQLQueue.ts +1 -1
  102. package/src/services/QueueMaker/memQueue.ts +1 -1
  103. package/src/services/QueueMaker/sbqueue.ts +7 -7
  104. package/src/services/Store/Cosmos.ts +1 -1
  105. package/src/services/Store/Disk.ts +1 -1
  106. package/src/services/adapters/SQL/Model.ts +939 -0
  107. package/src/services/adapters/SQL.ts +1 -0
  108. package/src/services/adapters/ServiceBus.ts +140 -0
  109. package/src/services/adapters/cosmos-client.ts +16 -0
  110. package/src/services/adapters/index.ts +0 -0
  111. package/src/services/adapters/logger.ts +3 -0
  112. package/src/services/adapters/memQueue.ts +26 -0
  113. package/src/services/adapters/mongo-client.ts +23 -0
  114. package/src/services/adapters/redis-client.ts +123 -0
  115. package/tsconfig.src.json +0 -3
  116. package/src/services/Store/Redis.ts.bak +0 -88
  117. package/src/services/simpledb/cosmosdb.ts.bak +0 -149
  118. package/src/services/simpledb/diskdb.ts.bak +0 -165
  119. package/src/services/simpledb/index.ts.bak +0 -6
  120. package/src/services/simpledb/memdb.ts.bak +0 -78
  121. package/src/services/simpledb/mongodb.ts.bak +0 -107
  122. package/src/services/simpledb/redisdb.ts.bak +0 -202
  123. package/src/services/simpledb/shared.ts.bak +0 -117
  124. 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,3 @@
1
+ import { makeLog } from "effect-app/utils/logger"
2
+
3
+ export const InfraLogger = makeLog("@effect-app/infra", "info")
@@ -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
@@ -34,8 +34,5 @@
34
34
  {
35
35
  "path": "../prelude"
36
36
  },
37
- {
38
- "path": "../infra-adapters"
39
- }
40
37
  ]
41
38
  }
@@ -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
- }