@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
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { flow, pipe } from "effect-app/Function"
|
|
2
|
-
import fs from "fs"
|
|
3
|
-
import * as PLF from "proper-lockfile"
|
|
4
|
-
|
|
5
|
-
import { pretty } from "effect-app/utils"
|
|
6
|
-
import * as fu from "@effect-app/infra-adapters/fileUtil"
|
|
7
|
-
import { Effect, Option } from "effect-app"
|
|
8
|
-
import type { CachedRecord, DBRecord, Index } from "./shared.js"
|
|
9
|
-
import { ConnectionException, CouldNotAquireDbLockException, getIndexName, getRecordName } from "./shared.js"
|
|
10
|
-
import * as simpledb from "./simpledb.js"
|
|
11
|
-
import type { Version } from "./simpledb.js"
|
|
12
|
-
|
|
13
|
-
export function createContext<TKey extends string, EA, A extends DBRecord<TKey>>() {
|
|
14
|
-
return <REncode, RDecode, EDecode>(
|
|
15
|
-
type: string,
|
|
16
|
-
encode: (record: A) => Effect<EA, never, REncode>,
|
|
17
|
-
decode: (d: EA) => Effect<A, EDecode, RDecode>,
|
|
18
|
-
schemaVersion: string,
|
|
19
|
-
makeIndexKey: (r: A) => Index,
|
|
20
|
-
dir = "./data.js"
|
|
21
|
-
) => {
|
|
22
|
-
initialise(dir)
|
|
23
|
-
const globalLock = "global.lock"
|
|
24
|
-
const typeLockKey = getIdxName(type, globalLock)
|
|
25
|
-
if (!fs.existsSync(typeLockKey)) {
|
|
26
|
-
fs.writeFileSync(typeLockKey, "", "utf-8")
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
find: simpledb.find(find(type), decode, type),
|
|
31
|
-
findByIndex: getIdx,
|
|
32
|
-
save: simpledb.store(find(type), store, lockRecordOnDisk(type), type)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function store(record: A, currentVersion: Option<Version>) {
|
|
36
|
-
const version = currentVersion
|
|
37
|
-
.map((cv) => (parseInt(cv) + 1).toString())
|
|
38
|
-
.getOrElse(() => "1")
|
|
39
|
-
const getData = flow(
|
|
40
|
-
encode,
|
|
41
|
-
(_) => _.map((data) => pretty({ version, timestamp: new Date(), data }))
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
const idx = makeIndexKey(record)
|
|
45
|
-
return currentVersion.isSome()
|
|
46
|
-
? lockIndex(record)
|
|
47
|
-
.zipRight(
|
|
48
|
-
readIndex(idx)
|
|
49
|
-
.flatMap((x) =>
|
|
50
|
-
x[record.id]
|
|
51
|
-
? Effect.fail(() => new Error("Combination already exists, abort"))
|
|
52
|
-
: getData(record)
|
|
53
|
-
.flatMap((serialised) => fu.writeTextFile(getFilename(type, record.id), serialised))
|
|
54
|
-
.zipRight(writeIndex(idx, { ...x, [idx.key]: record.id }))
|
|
55
|
-
)
|
|
56
|
-
.orDie
|
|
57
|
-
)
|
|
58
|
-
.scoped
|
|
59
|
-
.map(() => ({ version, data: record } as CachedRecord<A>))
|
|
60
|
-
: getData(record)
|
|
61
|
-
.flatMap((serialised) => fu.writeTextFile(getFilename(type, record.id), serialised))
|
|
62
|
-
.map(() => ({ version, data: record } as CachedRecord<A>))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function lockIndex(record: A) {
|
|
66
|
-
const index = makeIndexKey(record)
|
|
67
|
-
return lockDiskIndex(index)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function lockDiskIndex(_: Index) {
|
|
71
|
-
/*
|
|
72
|
-
Disk index locks require a file to exist already, hence for now we use a global index lock.
|
|
73
|
-
*/
|
|
74
|
-
// const lockKey = getIdxKey(index)
|
|
75
|
-
const lockKey = globalLock
|
|
76
|
-
return lockIndexOnDisk(type)(lockKey)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function lockRecordOnDisk(type: string) {
|
|
80
|
-
return (id: string) =>
|
|
81
|
-
lockFile(getFilename(type, id))
|
|
82
|
-
.mapBoth({
|
|
83
|
-
onFailure: (err) => new CouldNotAquireDbLockException(type, id, err as Error),
|
|
84
|
-
onSuccess: (release) => ({ release })
|
|
85
|
-
})
|
|
86
|
-
.acquireRelease(
|
|
87
|
-
(l) => l.release
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function lockIndexOnDisk(type: string) {
|
|
92
|
-
return (id: string) =>
|
|
93
|
-
lockFile(getIdxName(type, id))
|
|
94
|
-
.mapBoth({
|
|
95
|
-
onFailure: (err) => new CouldNotAquireDbLockException(type, id, err as Error),
|
|
96
|
-
onSuccess: (release) => ({ release })
|
|
97
|
-
})
|
|
98
|
-
.acquireRelease(
|
|
99
|
-
(l) => l.release
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function readFile(filePath: string) {
|
|
104
|
-
return fu
|
|
105
|
-
.readTextFile(filePath)
|
|
106
|
-
.catchAll((err) => Effect.die(new ConnectionException(err as Error)))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function find(type: string) {
|
|
110
|
-
return (id: string) => {
|
|
111
|
-
return tryRead(getFilename(type, id)).map(
|
|
112
|
-
(_) => _.map((s) => JSON.parse(s) as CachedRecord<EA>)
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function getIdx(index: Index) {
|
|
118
|
-
return readIndex(index).map((idx) => Option.fromNullable(idx[index.key]))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function readIndex(index: Index) {
|
|
122
|
-
return tryRead(getIdxName(type, index.doc)).map(
|
|
123
|
-
(_) =>
|
|
124
|
-
_.match(
|
|
125
|
-
{ onNone: () => ({} as Record<string, TKey>), onSome: (x) => JSON.parse(x) as Record<string, TKey> }
|
|
126
|
-
)
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function writeIndex(index: Index, content: Record<string, TKey>) {
|
|
131
|
-
return pipe(JSON.stringify(content), (serialised) => fu.writeTextFile(getIdxName(type, index.doc), serialised))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function tryRead(filePath: string) {
|
|
135
|
-
return fu
|
|
136
|
-
.fileExists(filePath)
|
|
137
|
-
.flatMap((exists) => !exists ? Effect.sync(() => Option.none()) : readFile(filePath).map(Option.some))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function getFilename(type: string, id: string) {
|
|
141
|
-
return `${dir}/v${schemaVersion}.${getRecordName(type, id)}.json`
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function getIdxName(type: string, id: string) {
|
|
145
|
-
return `${dir}/v${schemaVersion}.${getIndexName(type, id)}.json`
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function lockFile(fileName: string) {
|
|
151
|
-
return Effect.tryPromise(() => PLF.lock(fileName).then(flow(Effect.tryPromise, Effect.orDie)))
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// TODO: ugh.
|
|
155
|
-
let initialised = false
|
|
156
|
-
export function initialise(dir: string) {
|
|
157
|
-
if (initialised) {
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!fs.existsSync(dir)) {
|
|
162
|
-
fs.mkdirSync(dir)
|
|
163
|
-
}
|
|
164
|
-
initialised = true
|
|
165
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { flow } from "effect-app/Function"
|
|
2
|
-
|
|
3
|
-
import { Effect, Option } from "effect-app"
|
|
4
|
-
import type { Equivalence } from "effect-app"
|
|
5
|
-
import type { CachedRecord, DBRecord } from "./shared.js"
|
|
6
|
-
import { getRecordName, makeMap, SerializedDBRecord } from "./shared.js"
|
|
7
|
-
import * as simpledb from "./simpledb.js"
|
|
8
|
-
import type { Version } from "./simpledb.js"
|
|
9
|
-
// When we are in-process, we want to share the same Storage
|
|
10
|
-
// Do not try this at home.
|
|
11
|
-
const storage = makeMap<string, string>()
|
|
12
|
-
|
|
13
|
-
const parseSDB = S.decodeUnknown(SerializedDBRecord)
|
|
14
|
-
|
|
15
|
-
export function createContext<TKey extends string, EA, A extends DBRecord<TKey>>() {
|
|
16
|
-
return <REncode, RDecode, EDecode>(
|
|
17
|
-
type: string,
|
|
18
|
-
encode: (record: A) => Effect<EA, never, REncode>,
|
|
19
|
-
decode: (d: EA) => Effect<A, EDecode, RDecode>
|
|
20
|
-
) => {
|
|
21
|
-
return {
|
|
22
|
-
find: simpledb.find(find, decode, type),
|
|
23
|
-
findBy,
|
|
24
|
-
save: simpledb.store(find, store, bogusLock, type)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function find(id: string) {
|
|
28
|
-
return storage
|
|
29
|
-
.find(getRecordName(type, id))
|
|
30
|
-
.map((_) => _.map((s) => JSON.parse(s)))
|
|
31
|
-
.flatMapOpt(parseSDB)
|
|
32
|
-
.mapOpt(({ data, version }) => ({
|
|
33
|
-
data: JSON.parse(data) as EA,
|
|
34
|
-
version
|
|
35
|
-
}))
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function findBy<V extends Partial<A>>(keys: V, eq: Equivalence<V>) {
|
|
39
|
-
// Naive implementation, fine for in memory testing purposes.
|
|
40
|
-
return Effect
|
|
41
|
-
.gen(function*($) {
|
|
42
|
-
for (const [, value] of storage) {
|
|
43
|
-
const sdb_ = JSON.parse(value)
|
|
44
|
-
const sdb = yield* $(parseSDB(sdb_))
|
|
45
|
-
const cr = { data: JSON.parse(sdb.data) as EA, version: sdb.version }
|
|
46
|
-
const r = yield* $(
|
|
47
|
-
decode(cr.data)
|
|
48
|
-
.filterOrFail((d) => eq(keys, d as unknown as V), () => "not equals")
|
|
49
|
-
.exit
|
|
50
|
-
)
|
|
51
|
-
if (r.isSuccess()) {
|
|
52
|
-
return r.value
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return null
|
|
56
|
-
})
|
|
57
|
-
.map(Option.fromNullable)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function store(record: A, currentVersion: Option<Version>) {
|
|
61
|
-
const version = currentVersion
|
|
62
|
-
.map((cv) => (parseInt(cv) + 1).toString())
|
|
63
|
-
.getOrElse(() => "1")
|
|
64
|
-
|
|
65
|
-
const getData = flow(
|
|
66
|
-
encode,
|
|
67
|
-
(_) => _.map(JSON.stringify).map((data) => JSON.stringify({ version, timestamp: new Date(), data }))
|
|
68
|
-
)
|
|
69
|
-
return getData(record)
|
|
70
|
-
.flatMap((serialised) => storage.set(getRecordName(type, record.id), serialised))
|
|
71
|
-
.map(() => ({ version, data: record } as CachedRecord<A>))
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function bogusLock() {
|
|
77
|
-
return Effect.unit.acquireRelease(() => Effect.unit)
|
|
78
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { MongoClient } from "@effect-app/infra-adapters/mongo-client"
|
|
2
|
-
import { Effect, Option } from "effect-app"
|
|
3
|
-
import type { IndexDescription, InsertOneOptions } from "mongodb"
|
|
4
|
-
import type { CachedRecord, DBRecord } from "./shared.js"
|
|
5
|
-
import { OptimisticLockException } from "./shared.js"
|
|
6
|
-
import * as simpledb from "./simpledb.js"
|
|
7
|
-
import type { Version } from "./simpledb.js"
|
|
8
|
-
|
|
9
|
-
// const makeFromIndexKeys = (indexKeys: string[], unique: boolean) => indexKeys.reduce((prev, cur) => {
|
|
10
|
-
// prev[cur] = 1
|
|
11
|
-
// return prev
|
|
12
|
-
// }, {} as Record<string, number>)
|
|
13
|
-
|
|
14
|
-
const setup = (type: string, indexes: IndexDescription[]) =>
|
|
15
|
-
MongoClient
|
|
16
|
-
.tap(({ db }) => Effect.tryPromise(() => db.createCollection(type).catch((err) => console.warn(err))))
|
|
17
|
-
.flatMap(({ db }) => Effect.tryPromise(() => db.collection(type).createIndexes(indexes)))
|
|
18
|
-
|
|
19
|
-
export function createContext<TKey extends string, EA, A extends DBRecord<TKey>>() {
|
|
20
|
-
return <REncode, RDecode, EDecode>(
|
|
21
|
-
type: string,
|
|
22
|
-
encode: (record: A) => Effect<EA, never, REncode>,
|
|
23
|
-
decode: (d: EA) => Effect<A, EDecode, RDecode>,
|
|
24
|
-
// schemaVersion: string,
|
|
25
|
-
indexes: IndexDescription[]
|
|
26
|
-
) => {
|
|
27
|
-
return setup(type, indexes).map(() => ({
|
|
28
|
-
find: simpledb.find(find, decode, type),
|
|
29
|
-
findBy,
|
|
30
|
-
save: simpledb.storeDirectly(store, type)
|
|
31
|
-
}))
|
|
32
|
-
|
|
33
|
-
function find(id: string) {
|
|
34
|
-
return MongoClient
|
|
35
|
-
.flatMap(({ db }) =>
|
|
36
|
-
Effect.tryPromise(() =>
|
|
37
|
-
db
|
|
38
|
-
.collection(type)
|
|
39
|
-
.findOne<{ _id: TKey; version: Version; data: EA }>({ _id: { equals: id } })
|
|
40
|
-
)
|
|
41
|
-
)
|
|
42
|
-
.map(Option.fromNullable)
|
|
43
|
-
.mapOpt(({ data, version }) => ({ version, data } as CachedRecord<EA>))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function findBy(keys: Record<string, string>) {
|
|
47
|
-
return MongoClient
|
|
48
|
-
.flatMap(({ db }) =>
|
|
49
|
-
Effect.tryPromise(() => db.collection(type).findOne<{ _id: TKey }>(keys, { projection: { _id: 1 } }))
|
|
50
|
-
)
|
|
51
|
-
.map(Option.fromNullable)
|
|
52
|
-
.mapOpt(({ _id }) => _id)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function store(record: A, currentVersion: Option<Version>) {
|
|
56
|
-
return Effect.gen(function*($) {
|
|
57
|
-
const version = currentVersion
|
|
58
|
-
.map((cv) => (parseInt(cv) + 1).toString())
|
|
59
|
-
.getOrElse(() => "1")
|
|
60
|
-
|
|
61
|
-
const { db } = yield* $(MongoClient)
|
|
62
|
-
const data = yield* $(encode(record))
|
|
63
|
-
yield* $(
|
|
64
|
-
currentVersion.match(
|
|
65
|
-
{
|
|
66
|
-
onNone: () =>
|
|
67
|
-
Effect
|
|
68
|
-
.tryPromise(() =>
|
|
69
|
-
db
|
|
70
|
-
.collection(type)
|
|
71
|
-
.insertOne(
|
|
72
|
-
{ _id: record.id as any, version, timestamp: new Date(), data },
|
|
73
|
-
{
|
|
74
|
-
checkKeys: false // support for keys with `.` and `$`. NOTE: you can write them, read them, but NOT query for them.
|
|
75
|
-
} as InsertOneOptions
|
|
76
|
-
)
|
|
77
|
-
)
|
|
78
|
-
.asUnit
|
|
79
|
-
.orDie,
|
|
80
|
-
onSome: (currentVersion) =>
|
|
81
|
-
Effect
|
|
82
|
-
.tryPromise(() =>
|
|
83
|
-
db.collection(type).replaceOne(
|
|
84
|
-
{ _id: record.id as any, version: currentVersion },
|
|
85
|
-
{
|
|
86
|
-
version,
|
|
87
|
-
timestamp: new Date(),
|
|
88
|
-
data
|
|
89
|
-
},
|
|
90
|
-
{ upsert: false }
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
.orDie
|
|
94
|
-
.flatMap((x) => {
|
|
95
|
-
if (!x.modifiedCount) {
|
|
96
|
-
return new OptimisticLockException(type, record.id)
|
|
97
|
-
}
|
|
98
|
-
return Effect.unit
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
)
|
|
103
|
-
return { version, data: record } as CachedRecord<A>
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
|
|
3
|
-
import { flow } from "effect-app/Function"
|
|
4
|
-
import { RedisClient } from "@effect-app/infra-adapters/redis-client"
|
|
5
|
-
import { Effect } from "effect-app"
|
|
6
|
-
import type { Option } from "effect-app"
|
|
7
|
-
import * as S from "effect-app/Schema"
|
|
8
|
-
import type { Lock } from "redlock"
|
|
9
|
-
import type { CachedRecord, DBRecord, Index } from "./shared.js"
|
|
10
|
-
import { ConnectionException, CouldNotAquireDbLockException, getIndexName, getRecordName } from "./shared.js"
|
|
11
|
-
import * as simpledb from "./simpledb.js"
|
|
12
|
-
|
|
13
|
-
const ttl = 10 * 1000
|
|
14
|
-
|
|
15
|
-
export function createContext<TKey extends string, EA, A extends DBRecord<TKey>>() {
|
|
16
|
-
return <REncode, RDecode, EDecode>(
|
|
17
|
-
type: string,
|
|
18
|
-
encode: (record: A) => Effect<EA, never, REncode>,
|
|
19
|
-
decode: (d: EA) => Effect<A, EDecode, RDecode>,
|
|
20
|
-
schemaVersion: string,
|
|
21
|
-
makeIndexKey: (r: A) => Index
|
|
22
|
-
) => {
|
|
23
|
-
const getData = flow(encode, (_) => _.map(JSON.stringify))
|
|
24
|
-
return {
|
|
25
|
-
find: simpledb.find(find, decode, type),
|
|
26
|
-
findByIndex: getIdx,
|
|
27
|
-
save: simpledb.store(find, store, lockRedisRecord, type)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function find(id: string) {
|
|
31
|
-
return Effect
|
|
32
|
-
.flatMap(RedisClient, (_) => _.hmgetAll(getKey(id)))
|
|
33
|
-
.flatMapOpt((v) =>
|
|
34
|
-
RedisSerializedDBRecord
|
|
35
|
-
.decodeUnknown(v)
|
|
36
|
-
.map(({ data, version }) => ({
|
|
37
|
-
data: JSON.parse(data) as EA,
|
|
38
|
-
version
|
|
39
|
-
}))
|
|
40
|
-
.mapError((e) => new ConnectionException(new Error(e.toString())))
|
|
41
|
-
)
|
|
42
|
-
.orDie
|
|
43
|
-
}
|
|
44
|
-
function store(record: A, currentVersion: Option<string>) {
|
|
45
|
-
const version = currentVersion
|
|
46
|
-
.map((cv) => (parseInt(cv) + 1).toString())
|
|
47
|
-
.getOrElse(() => "1")
|
|
48
|
-
return currentVersion.match(
|
|
49
|
-
{
|
|
50
|
-
onNone: () =>
|
|
51
|
-
lockIndex(record)
|
|
52
|
-
.zipRight(
|
|
53
|
-
getIndex(record)
|
|
54
|
-
.zipRightOpt(
|
|
55
|
-
Effect.fail(() => new Error("Combination already exists, abort"))
|
|
56
|
-
)
|
|
57
|
-
.zipRight(getData(record))
|
|
58
|
-
// TODO: instead use MULTI & EXEC to make it in one command?
|
|
59
|
-
.flatMap((data) =>
|
|
60
|
-
hmSetRec(
|
|
61
|
-
getKey(record.id),
|
|
62
|
-
new RedisSerializedDBRecord({
|
|
63
|
-
version,
|
|
64
|
-
timestamp: new Date(),
|
|
65
|
-
data
|
|
66
|
-
})
|
|
67
|
-
)
|
|
68
|
-
)
|
|
69
|
-
.zipRight(setIndex(record))
|
|
70
|
-
.orDie
|
|
71
|
-
.map(() => ({ version, data: record } as CachedRecord<A>))
|
|
72
|
-
)
|
|
73
|
-
.scoped,
|
|
74
|
-
onSome: () =>
|
|
75
|
-
getData(record)
|
|
76
|
-
.flatMap((data) =>
|
|
77
|
-
hmSetRec(
|
|
78
|
-
getKey(record.id),
|
|
79
|
-
new RedisSerializedDBRecord({
|
|
80
|
-
version,
|
|
81
|
-
timestamp: new Date(),
|
|
82
|
-
data
|
|
83
|
-
})
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
.orDie
|
|
87
|
-
.map(() => ({ version, data: record } as CachedRecord<A>))
|
|
88
|
-
}
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getIndex(record: A) {
|
|
93
|
-
const index = makeIndexKey(record)
|
|
94
|
-
return getIdx(index)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function setIndex(record: A) {
|
|
98
|
-
const index = makeIndexKey(record)
|
|
99
|
-
return setIdx(index, record)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function lockIndex(record: A) {
|
|
103
|
-
const index = makeIndexKey(record)
|
|
104
|
-
return lockRedisIdx(index)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function getIdx(index: Index) {
|
|
108
|
-
return Effect.flatMap(RedisClient, (_) => _.hget(getIdxKey(index), index.key).map((_) => _.map((i) => i as TKey)))
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function setIdx(index: Index, r: A) {
|
|
112
|
-
return Effect.flatMap(RedisClient, (_) => _.hset(getIdxKey(index), index.key, r.id))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function lockRedisIdx(index: Index) {
|
|
116
|
-
const lockKey = getIdxLockKey(index)
|
|
117
|
-
// acquire
|
|
118
|
-
return Effect
|
|
119
|
-
.flatMap(
|
|
120
|
-
RedisClient,
|
|
121
|
-
({ lock }) => Effect.tryPromise(() => lock.lock(lockKey, ttl) as unknown as Promise<Lock>)
|
|
122
|
-
)
|
|
123
|
-
.mapBoth({
|
|
124
|
-
onFailure: (err) => new CouldNotAquireDbLockException(type, lockKey, err as Error),
|
|
125
|
-
// release
|
|
126
|
-
onSuccess: (lock) => ({
|
|
127
|
-
release: Effect
|
|
128
|
-
.tryPromise(() => lock.unlock() as unknown as Promise<void>)
|
|
129
|
-
.orDie
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
.acquireRelease(
|
|
133
|
-
(l) => l.release
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function lockRedisRecord(id: string) {
|
|
138
|
-
// acquire
|
|
139
|
-
return Effect
|
|
140
|
-
.flatMap(RedisClient, ({ lock }) =>
|
|
141
|
-
Effect.tryPromise(
|
|
142
|
-
() => lock.lock(getLockKey(id), ttl) as unknown as Promise<Lock>
|
|
143
|
-
))
|
|
144
|
-
.mapBoth({
|
|
145
|
-
onFailure: (err) => new CouldNotAquireDbLockException(type, id, err as Error),
|
|
146
|
-
// release
|
|
147
|
-
onSuccess: (lock) => ({
|
|
148
|
-
// TODO
|
|
149
|
-
release: Effect
|
|
150
|
-
.tryPromise(() => lock.unlock() as unknown as Promise<void>)
|
|
151
|
-
.orDie
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
.acquireRelease(
|
|
155
|
-
(l) => l.release
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getKey(id: string) {
|
|
160
|
-
return `v${schemaVersion}.${getRecordName(type, id)}`
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function getLockKey(id: string) {
|
|
164
|
-
return `v${schemaVersion}.locks.${getRecordName(type, id)}`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getIdxKey(index: Index) {
|
|
168
|
-
return `v${schemaVersion}.${getIndexName(type, index.doc)}`
|
|
169
|
-
}
|
|
170
|
-
function getIdxLockKey(index: Index) {
|
|
171
|
-
return `v${schemaVersion}.locks.${getIndexName(type, index.doc)}_${index.key}`
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function hmSetRec(key: string, val: RedisSerializedDBRecord) {
|
|
176
|
-
const enc = S.encodeSync(RedisSerializedDBRecord)(val)
|
|
177
|
-
return Effect.flatMap(RedisClient, ({ client }) =>
|
|
178
|
-
Effect
|
|
179
|
-
.async<void, ConnectionException>((res) => {
|
|
180
|
-
client.hmset(
|
|
181
|
-
key,
|
|
182
|
-
"version",
|
|
183
|
-
enc.version,
|
|
184
|
-
"timestamp",
|
|
185
|
-
enc.timestamp,
|
|
186
|
-
"data",
|
|
187
|
-
enc.data,
|
|
188
|
-
(err) =>
|
|
189
|
-
err
|
|
190
|
-
? res(new ConnectionException(err))
|
|
191
|
-
: res(Effect.sync(() => void 0))
|
|
192
|
-
)
|
|
193
|
-
})
|
|
194
|
-
.uninterruptible)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export class RedisSerializedDBRecord extends S.Class<RedisSerializedDBRecord>()({
|
|
199
|
-
version: S.string,
|
|
200
|
-
timestamp: S.Date,
|
|
201
|
-
data: S.string
|
|
202
|
-
}) {}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { Data, Effect, Option } from "effect-app"
|
|
2
|
-
import * as S from "effect-app/Schema"
|
|
3
|
-
|
|
4
|
-
export class CouldNotAquireDbLockException
|
|
5
|
-
extends Data.TaggedError("CouldNotAquireDbLockException")<{ type: string; id: string; error: Error; message: string }>
|
|
6
|
-
{
|
|
7
|
-
constructor(type: string, id: string, error: Error) {
|
|
8
|
-
super({ type, id, error, message: `Couldn't lock db record ${type}: ${id}` })
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class OptimisticLockException
|
|
13
|
-
extends Data.TaggedError("OptimisticLockException")<{ type: string; id: string; message: string }>
|
|
14
|
-
{
|
|
15
|
-
constructor(type: string, id: string) {
|
|
16
|
-
super({ type, id, message: `Existing ${type} ${id} record changed` })
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class ConnectionException extends Data.TaggedError("ConnectionException")<{ cause: Error; message: string }> {
|
|
21
|
-
readonly _errorTag = "ConnectionException"
|
|
22
|
-
constructor(cause: Error) {
|
|
23
|
-
super({ cause, message: "A connection error ocurred" })
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface DBRecord<TKey extends string> {
|
|
28
|
-
id: TKey
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class SerializedDBRecord extends S.Class<SerializedDBRecord>()({
|
|
32
|
-
version: S.string,
|
|
33
|
-
timestamp: S.Date,
|
|
34
|
-
data: S.string
|
|
35
|
-
}) {}
|
|
36
|
-
|
|
37
|
-
// unknown -> string -> SDB?
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
export function makeSerialisedDBRecord(s: S.Schema<any>) {
|
|
40
|
-
return S.Struct({
|
|
41
|
-
version: S.number,
|
|
42
|
-
timestamp: S.Date,
|
|
43
|
-
data: s
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface CachedRecord<T> {
|
|
48
|
-
version: string
|
|
49
|
-
data: T
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface Index {
|
|
53
|
-
doc: string
|
|
54
|
-
key: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getIndexName(type: string, id: string) {
|
|
58
|
-
return `${type}-idx_${id}`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function getRecordName(type: string, id: string) {
|
|
62
|
-
return `${type}-r_${id}`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function makeMap<TKey, T>() {
|
|
66
|
-
const map = new Map<TKey, T>()
|
|
67
|
-
return {
|
|
68
|
-
find: (k: TKey) => Effect.sync(() => Option.fromNullable(map.get(k))),
|
|
69
|
-
[Symbol.iterator]: () => map[Symbol.iterator](),
|
|
70
|
-
set: (k: TKey, v: T) =>
|
|
71
|
-
Effect.sync(() => {
|
|
72
|
-
map.set(k, v)
|
|
73
|
-
})
|
|
74
|
-
} as EffectMap<TKey, T>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface EffectMap<TKey, T> {
|
|
78
|
-
[Symbol.iterator](): IterableIterator<[TKey, T]>
|
|
79
|
-
find: (k: TKey) => Effect<Option<T>>
|
|
80
|
-
set: (k: TKey, v: T) => Effect<void>
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// export function encodeOnlyWhenStrictMatch<A, E>(
|
|
84
|
-
// encode: S.HasEncoder<A, E>["encode_"],
|
|
85
|
-
// v: A
|
|
86
|
-
// ) {
|
|
87
|
-
// const e1 = Sync.run(encode(v, "strict"))
|
|
88
|
-
// const e2 = Sync.run(encode(v, "classic"))
|
|
89
|
-
// try {
|
|
90
|
-
// assert.deepStrictEqual(e1, e2)
|
|
91
|
-
// } catch (err) {
|
|
92
|
-
// throw new Error(
|
|
93
|
-
// // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
94
|
-
// "The strict encoding of these objects does not match the classic encoding of these objects. This means that there is a chance of a data-loss, and is probably a programming error\n" +
|
|
95
|
-
// err
|
|
96
|
-
// )
|
|
97
|
-
// }
|
|
98
|
-
// return e1
|
|
99
|
-
// }
|
|
100
|
-
|
|
101
|
-
// export function decodeOnlyWhenStrictMatch<A, E>(
|
|
102
|
-
// decode: S.HasDecoder<A, E>["decode_"],
|
|
103
|
-
// u: unknown
|
|
104
|
-
// ) {
|
|
105
|
-
// return pipe(
|
|
106
|
-
// decode(u, "strict"),
|
|
107
|
-
// Sync.tap((v) =>
|
|
108
|
-
// pipe(
|
|
109
|
-
// decode(u),
|
|
110
|
-
// Sync.tap((v2) => {
|
|
111
|
-
// assert.deepStrictEqual(v, v2)
|
|
112
|
-
// return Sync.succeed(v2)
|
|
113
|
-
// })
|
|
114
|
-
// )
|
|
115
|
-
// )
|
|
116
|
-
// )
|
|
117
|
-
// }
|