@effect-app/infra 4.0.0-beta.266 → 4.0.0-beta.268
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 +22 -0
- package/dist/ClusterCosmos.d.ts +16 -16
- package/dist/Store/Cosmos.d.ts +1 -1
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +64 -31
- package/dist/Store/Disk.d.ts +2 -2
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +2 -2
- package/dist/Store/Memory.d.ts +2 -3
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +3 -6
- package/dist/Store/SQL.d.ts +1 -1
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +14 -7
- package/dist/Store/codeFilter.d.ts +1 -1
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +2 -3
- package/dist/Store/index.js +1 -1
- package/dist/Store/utils.d.ts +3 -1
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +5 -1
- package/dist/WorkflowEngineCosmos.d.ts +1 -1
- package/dist/routing/middleware.d.ts +2 -2
- package/package.json +3 -3
- package/src/CUPS.ts +1 -1
- package/src/ClusterCosmos.ts +2 -2
- package/src/ContextProvider.ts +1 -1
- package/src/Emailer/Sendgrid.ts +1 -1
- package/src/Emailer/fake.ts +1 -1
- package/src/MainFiberSet.ts +3 -3
- package/src/QueueMaker/SQLQueue.ts +3 -3
- package/src/QueueMaker/memQueue.ts +4 -4
- package/src/QueueMaker/sbqueue.ts +4 -4
- package/src/RequestFiberSet.ts +2 -2
- package/src/SQL/Model.ts +1 -1
- package/src/SQL.ts +1 -1
- package/src/ServiceBus.ts +1 -1
- package/src/Store/Cosmos/query.ts +2 -2
- package/src/Store/Cosmos.ts +88 -64
- package/src/Store/Disk.ts +6 -4
- package/src/Store/Memory.ts +5 -8
- package/src/Store/SQL/Pg.ts +5 -5
- package/src/Store/SQL/query.ts +2 -2
- package/src/Store/SQL.ts +23 -12
- package/src/Store/codeFilter.ts +1 -2
- package/src/Store/index.test.ts.bak +2 -2
- package/src/Store/index.ts +6 -6
- package/src/Store/utils.ts +6 -1
- package/src/WorkflowEngineCosmos.ts +3 -3
- package/src/WorkflowEngineSqlite.ts +2 -2
- package/src/errorReporter.ts +2 -2
- package/src/index.ts +2 -2
- package/src/internal/events.ts +1 -1
- package/src/layerUtils.ts +1 -1
- package/src/logger/jsonLogger.ts +1 -1
- package/src/logger/logFmtLogger.ts +1 -1
- package/src/middlewares.ts +4 -4
- package/src/reportError.ts +1 -1
- package/src/routing/middleware/middleware.ts +3 -3
- package/src/routing.ts +3 -3
- package/src/test.ts +2 -2
- package/src/vitest.ts +1 -1
package/src/Emailer/Sendgrid.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { dropUndefinedT } from "effect-app/utils"
|
|
|
7
7
|
import * as Equivalence from "effect/Equivalence"
|
|
8
8
|
import * as Redacted from "effect/Redacted"
|
|
9
9
|
import { inspect } from "util"
|
|
10
|
-
import { InfraLogger } from "../logger.
|
|
10
|
+
import { InfraLogger } from "../logger.ts"
|
|
11
11
|
|
|
12
12
|
export interface SendgridConfig {
|
|
13
13
|
defaultReplyTo?: EmailData
|
package/src/Emailer/fake.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect-app/Effect"
|
|
2
2
|
import { Emailer } from "effect-app/Emailer"
|
|
3
3
|
import { pretty } from "effect-app/utils"
|
|
4
|
-
import { InfraLogger } from "../logger.
|
|
4
|
+
import { InfraLogger } from "../logger.ts"
|
|
5
5
|
|
|
6
6
|
const makeFake = InfraLogger
|
|
7
7
|
.logInfo("FAKE Emailer Service enabled")
|
package/src/MainFiberSet.ts
CHANGED
|
@@ -4,9 +4,9 @@ import * as Layer from "effect-app/Layer"
|
|
|
4
4
|
import * as Fiber from "effect/Fiber"
|
|
5
5
|
import * as FiberSet from "effect/FiberSet"
|
|
6
6
|
|
|
7
|
-
import { InfraLogger } from "./logger.
|
|
8
|
-
import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.
|
|
9
|
-
import { setRootParentSpan } from "./RequestFiberSet.
|
|
7
|
+
import { InfraLogger } from "./logger.ts"
|
|
8
|
+
import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.ts"
|
|
9
|
+
import { setRootParentSpan } from "./RequestFiberSet.ts"
|
|
10
10
|
|
|
11
11
|
const make = Effect.gen(function*() {
|
|
12
12
|
const set = yield* FiberSet.make<unknown, never>()
|
|
@@ -11,9 +11,9 @@ import { pretty } from "effect-app/utils"
|
|
|
11
11
|
import * as Fiber from "effect/Fiber"
|
|
12
12
|
import * as Tracer from "effect/Tracer"
|
|
13
13
|
import { SqlClient } from "effect/unstable/sql"
|
|
14
|
-
import { InfraLogger } from "../logger.
|
|
15
|
-
import { messagingSpanArgs } from "../otel.
|
|
16
|
-
import { SQLModel } from "../SQL.
|
|
14
|
+
import { InfraLogger } from "../logger.ts"
|
|
15
|
+
import { messagingSpanArgs } from "../otel.ts"
|
|
16
|
+
import { SQLModel } from "../SQL.ts"
|
|
17
17
|
|
|
18
18
|
export const QueueId = S.Finite.pipe(S.brand("QueueId"))
|
|
19
19
|
export type QueueId = typeof QueueId.Type
|
|
@@ -9,10 +9,10 @@ import * as Fiber from "effect/Fiber"
|
|
|
9
9
|
import { flow } from "effect/Function"
|
|
10
10
|
import * as Q from "effect/Queue"
|
|
11
11
|
import * as Tracer from "effect/Tracer"
|
|
12
|
-
import { InfraLogger } from "../logger.
|
|
13
|
-
import { MemQueue } from "../memQueue.
|
|
14
|
-
import { messagingSpanArgs } from "../otel.
|
|
15
|
-
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause } from "./errors.
|
|
12
|
+
import { InfraLogger } from "../logger.ts"
|
|
13
|
+
import { MemQueue } from "../memQueue.ts"
|
|
14
|
+
import { messagingSpanArgs } from "../otel.ts"
|
|
15
|
+
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause } from "./errors.ts"
|
|
16
16
|
|
|
17
17
|
export const makeMemQueue = Effect.fnUntraced(function*<
|
|
18
18
|
Evt extends { id: S.StringId; _tag: string },
|
|
@@ -8,10 +8,10 @@ import { pretty } from "effect-app/utils"
|
|
|
8
8
|
import * as Cause from "effect/Cause"
|
|
9
9
|
import { flow } from "effect/Function"
|
|
10
10
|
import * as Tracer from "effect/Tracer"
|
|
11
|
-
import { InfraLogger } from "../logger.
|
|
12
|
-
import { messagingSpanArgs } from "../otel.
|
|
13
|
-
import { Receiver, Sender } from "../ServiceBus.
|
|
14
|
-
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause, reportQueueError } from "./errors.
|
|
11
|
+
import { InfraLogger } from "../logger.ts"
|
|
12
|
+
import { messagingSpanArgs } from "../otel.ts"
|
|
13
|
+
import { Receiver, Sender } from "../ServiceBus.ts"
|
|
14
|
+
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause, reportQueueError } from "./errors.ts"
|
|
15
15
|
|
|
16
16
|
export function makeServiceBusQueue<
|
|
17
17
|
Evt extends { id: StringId; _tag: string },
|
package/src/RequestFiberSet.ts
CHANGED
|
@@ -6,8 +6,8 @@ import * as Option from "effect-app/Option"
|
|
|
6
6
|
import * as Fiber from "effect/Fiber"
|
|
7
7
|
import * as FiberSet from "effect/FiberSet"
|
|
8
8
|
import type * as Tracer from "effect/Tracer"
|
|
9
|
-
import { InfraLogger } from "./logger.
|
|
10
|
-
import { reportRequestError, reportUnknownRequestError } from "./reportError.
|
|
9
|
+
import { InfraLogger } from "./logger.ts"
|
|
10
|
+
import { reportRequestError, reportUnknownRequestError } from "./reportError.ts"
|
|
11
11
|
|
|
12
12
|
const getRootParentSpan = Effect.gen(function*() {
|
|
13
13
|
let span: Tracer.AnySpan | null = yield* Effect.currentSpan.pipe(
|
package/src/SQL/Model.ts
CHANGED
|
@@ -24,7 +24,7 @@ import * as VariantSchema from "effect/unstable/schema/VariantSchema"
|
|
|
24
24
|
import { SqlClient } from "effect/unstable/sql/SqlClient"
|
|
25
25
|
import * as SqlResolver from "effect/unstable/sql/SqlResolver"
|
|
26
26
|
import * as SqlSchema from "effect/unstable/sql/SqlSchema"
|
|
27
|
-
import { type DbSystem, withDbSpan } from "../otel.
|
|
27
|
+
import { type DbSystem, withDbSpan } from "../otel.ts"
|
|
28
28
|
|
|
29
29
|
const {
|
|
30
30
|
Class,
|
package/src/SQL.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * as SQLModel from "./SQL/Model.
|
|
1
|
+
export * as SQLModel from "./SQL/Model.ts"
|
package/src/ServiceBus.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as Cause from "effect/Cause"
|
|
|
7
7
|
import * as Exit from "effect/Exit"
|
|
8
8
|
import * as FiberSet from "effect/FiberSet"
|
|
9
9
|
import type * as Scope from "effect/Scope"
|
|
10
|
-
import { InfraLogger } from "./logger.
|
|
10
|
+
import { InfraLogger } from "./logger.ts"
|
|
11
11
|
|
|
12
12
|
const logged = (name: string) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
13
13
|
Effect.logInfo(name).pipe(
|
|
@@ -7,8 +7,8 @@ import type { FilterR, FilterResult, Ops } from "effect-app/Model/filter/filterA
|
|
|
7
7
|
import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "effect-app/Model/query"
|
|
8
8
|
import type { SupportedValues } from "effect-app/Store"
|
|
9
9
|
import { assertUnreachable } from "effect-app/utils"
|
|
10
|
-
import { InfraLogger } from "../../logger.
|
|
11
|
-
import { isRelationCheck } from "../codeFilter.
|
|
10
|
+
import { InfraLogger } from "../../logger.ts"
|
|
11
|
+
import { isRelationCheck } from "../codeFilter.ts"
|
|
12
12
|
|
|
13
13
|
export function logQuery(q: {
|
|
14
14
|
query: string
|
package/src/Store/Cosmos.ts
CHANGED
|
@@ -14,11 +14,11 @@ import * as Duration from "effect/Duration"
|
|
|
14
14
|
import { pipe } from "effect/Function"
|
|
15
15
|
import * as Redacted from "effect/Redacted"
|
|
16
16
|
import * as Struct from "effect/Struct"
|
|
17
|
-
import { CosmosClient, CosmosClientLayer } from "../cosmos-client.
|
|
18
|
-
import { OptimisticConcurrencyException } from "../errors.
|
|
19
|
-
import { InfraLogger } from "../logger.
|
|
20
|
-
import { annotateCosmosResponse, annotateDb } from "../otel.
|
|
21
|
-
import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.
|
|
17
|
+
import { CosmosClient, CosmosClientLayer } from "../cosmos-client.ts"
|
|
18
|
+
import { DatabaseError, OptimisticConcurrencyException } from "../errors.ts"
|
|
19
|
+
import { InfraLogger } from "../logger.ts"
|
|
20
|
+
import { annotateCosmosResponse, annotateDb } from "../otel.ts"
|
|
21
|
+
import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.ts"
|
|
22
22
|
|
|
23
23
|
const makeMapId =
|
|
24
24
|
<IdKey extends keyof Encoded, Encoded extends FieldValues>(idKey: IdKey) => ({ [idKey]: id, ...e }: Encoded) => ({
|
|
@@ -30,9 +30,34 @@ const makeReverseMapId =
|
|
|
30
30
|
({ id, ...t }: PersistenceModelType<Omit<Encoded, IdKey> & { id: string }>) =>
|
|
31
31
|
({ ...t, [idKey]: id }) as any as PersistenceModelType<Encoded>
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Cosmos statuses worth retrying: request timeout, throttle, retry-with,
|
|
34
|
+
// internal, and service unavailable.
|
|
35
|
+
const RETRYABLE_COSMOS_STATUS = new Set([408, 429, 449, 500, 503])
|
|
36
|
+
const statusIsTransient = (code: number) => RETRYABLE_COSMOS_STATUS.has(code)
|
|
37
|
+
|
|
38
|
+
// A thrown SDK error is transient if its status/code is retryable or its message
|
|
39
|
+
// looks like a timeout / throttle / dropped connection.
|
|
40
|
+
const thrownIsTransient = (e: unknown): boolean => {
|
|
41
|
+
const code = (e as any)?.code ?? (e as any)?.statusCode
|
|
42
|
+
if (typeof code === "number" && RETRYABLE_COSMOS_STATUS.has(code)) return true
|
|
43
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
44
|
+
return /request timed out|timed out|\btimeout\b|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|socket hang up|ServiceUnavailable|RequestTimeout|TooManyRequests|throttl/i
|
|
45
|
+
.test(msg)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Wrap a Cosmos SDK promise so a rejection becomes a typed, serializable
|
|
49
|
+
// `DatabaseError` (transient-classified) instead of a bare-`Error` defect — the
|
|
50
|
+
// latter both bypasses retry and breaks JSON encoding of the failure channel.
|
|
51
|
+
const tryCosmos = <A>(f: () => Promise<A>): Effect.Effect<A, DatabaseError> =>
|
|
52
|
+
Effect.tryPromise({
|
|
53
|
+
try: f,
|
|
54
|
+
catch: (e) =>
|
|
55
|
+
new DatabaseError({
|
|
56
|
+
message: `Cosmos request failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
57
|
+
transient: thrownIsTransient(e),
|
|
58
|
+
cause: e
|
|
59
|
+
})
|
|
60
|
+
})
|
|
36
61
|
|
|
37
62
|
const respBytes = (
|
|
38
63
|
resp: { diagnostics?: { clientSideRequestStatistics?: { totalResponsePayloadLengthInBytes?: number } } }
|
|
@@ -209,8 +234,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
209
234
|
batches
|
|
210
235
|
.map((x, i) => [i, x] as const),
|
|
211
236
|
([i, batch]) =>
|
|
212
|
-
|
|
213
|
-
.promise(() => bulk(batch.map(([, op]) => op)))
|
|
237
|
+
tryCosmos(() => bulk(batch.map(([, op]) => op)))
|
|
214
238
|
.pipe(
|
|
215
239
|
Effect
|
|
216
240
|
.delay(Duration.millis(i === 0 ? 0 : 150)),
|
|
@@ -236,23 +260,21 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
236
260
|
(x) => x.statusCode !== 424 && (x.statusCode > 299 || x.statusCode < 200)
|
|
237
261
|
)
|
|
238
262
|
if (r2) {
|
|
239
|
-
return yield*
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
)
|
|
263
|
+
return yield* new DatabaseError({
|
|
264
|
+
message: "not able to update records: " + r2.statusCode,
|
|
265
|
+
transient: statusIsTransient(r2.statusCode),
|
|
266
|
+
cause: responses
|
|
267
|
+
})
|
|
245
268
|
}
|
|
246
269
|
const r3 = responses.find(
|
|
247
270
|
(x) => x.statusCode > 299 || x.statusCode < 200
|
|
248
271
|
)
|
|
249
272
|
if (r3) {
|
|
250
|
-
return yield*
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
273
|
+
return yield* new DatabaseError({
|
|
274
|
+
message: "not able to update records: " + r3.statusCode,
|
|
275
|
+
transient: statusIsTransient(r3.statusCode),
|
|
276
|
+
cause: responses
|
|
277
|
+
})
|
|
256
278
|
}
|
|
257
279
|
return batch.map(([e], i) => ({
|
|
258
280
|
...e,
|
|
@@ -317,8 +339,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
317
339
|
|
|
318
340
|
const ex = batch.map(([, c]) => c)
|
|
319
341
|
|
|
320
|
-
return
|
|
321
|
-
.promise(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
|
|
342
|
+
return tryCosmos(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
|
|
322
343
|
.pipe(Effect.flatMap(Effect.fnUntraced(function*(x) {
|
|
323
344
|
const result = x.result ?? []
|
|
324
345
|
const firstFailed = result.find(
|
|
@@ -330,9 +351,11 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
330
351
|
return yield* new OptimisticConcurrencyException({ type: name, id: "batch", code })
|
|
331
352
|
}
|
|
332
353
|
|
|
333
|
-
return yield*
|
|
334
|
-
|
|
335
|
-
|
|
354
|
+
return yield* new DatabaseError({
|
|
355
|
+
message: "not able to update record: " + code,
|
|
356
|
+
transient: statusIsTransient(code),
|
|
357
|
+
cause: x
|
|
358
|
+
})
|
|
336
359
|
}
|
|
337
360
|
|
|
338
361
|
return batch.map(([e], i) => ({
|
|
@@ -362,7 +385,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
362
385
|
Effect.flatMap(({ ns, q }) =>
|
|
363
386
|
Effect
|
|
364
387
|
.gen(function*() {
|
|
365
|
-
const response = yield*
|
|
388
|
+
const response = yield* tryCosmos(() =>
|
|
366
389
|
container.items.query<Out>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
|
|
367
390
|
)
|
|
368
391
|
yield* annotateFeed(response)
|
|
@@ -384,20 +407,19 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
384
407
|
),
|
|
385
408
|
batchRemove: (ids, partitionKey?: string) =>
|
|
386
409
|
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
partitionKey ?? nsBasePartitionKey(ns)
|
|
399
|
-
)
|
|
410
|
+
tryCosmos(() =>
|
|
411
|
+
execBatch(
|
|
412
|
+
mutable(ids.map((id) =>
|
|
413
|
+
dropUndefinedT({
|
|
414
|
+
operationType: "Delete" as const,
|
|
415
|
+
id
|
|
416
|
+
// don't use this or we get an error that the request and some item partition key dont match - makese no sense
|
|
417
|
+
// partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
|
|
418
|
+
})
|
|
419
|
+
)),
|
|
420
|
+
partitionKey ?? nsBasePartitionKey(ns)
|
|
400
421
|
)
|
|
422
|
+
)
|
|
401
423
|
.pipe(
|
|
402
424
|
annotateDb({
|
|
403
425
|
operation: "batchRemove",
|
|
@@ -421,7 +443,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
421
443
|
Effect.flatMap(({ ns, q }) =>
|
|
422
444
|
Effect
|
|
423
445
|
.gen(function*() {
|
|
424
|
-
const response = yield*
|
|
446
|
+
const response = yield* tryCosmos(() =>
|
|
425
447
|
container.items.query<PMCosmos>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
|
|
426
448
|
)
|
|
427
449
|
yield* annotateFeed(response)
|
|
@@ -482,7 +504,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
482
504
|
Effect
|
|
483
505
|
.gen(function*() {
|
|
484
506
|
if (f.select) {
|
|
485
|
-
const response = yield*
|
|
507
|
+
const response = yield* tryCosmos(() =>
|
|
486
508
|
container.items.query<M>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
|
|
487
509
|
)
|
|
488
510
|
yield* annotateFeed(response)
|
|
@@ -494,7 +516,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
494
516
|
...mapReverseId(_ as any)
|
|
495
517
|
}))
|
|
496
518
|
}
|
|
497
|
-
const response = yield*
|
|
519
|
+
const response = yield* tryCosmos(() =>
|
|
498
520
|
container.items.query<{ f: M }>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
|
|
499
521
|
)
|
|
500
522
|
yield* annotateFeed(response)
|
|
@@ -517,7 +539,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
517
539
|
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
518
540
|
Effect
|
|
519
541
|
.gen(function*() {
|
|
520
|
-
const response = yield*
|
|
542
|
+
const response = yield* tryCosmos(() =>
|
|
521
543
|
container
|
|
522
544
|
.item(id, nsPartitionValue(ns, { [idKey]: id } as Encoded))
|
|
523
545
|
.read<Encoded>()
|
|
@@ -541,20 +563,20 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
541
563
|
)),
|
|
542
564
|
set: (e) =>
|
|
543
565
|
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
566
|
+
// Annotate the result so the create/replace promise union collapses to
|
|
567
|
+
// one type (otherwise `A` infers as `unknown` and the chain degrades).
|
|
568
|
+
tryCosmos((): Promise<{ statusCode: number; etag: string }> =>
|
|
569
|
+
Option
|
|
570
|
+
.match(
|
|
571
|
+
Option
|
|
572
|
+
.fromNullishOr(e._etag),
|
|
573
|
+
{
|
|
574
|
+
onNone: () =>
|
|
551
575
|
container.items.create({
|
|
552
576
|
...mapId(e),
|
|
553
577
|
_partitionKey: nsPartitionValue(ns, e)
|
|
554
|
-
})
|
|
555
|
-
)
|
|
556
|
-
onSome: (eTag) =>
|
|
557
|
-
Effect.promise(() =>
|
|
578
|
+
}),
|
|
579
|
+
onSome: (eTag) =>
|
|
558
580
|
container.item(e[idKey], nsPartitionValue(ns, e)).replace(
|
|
559
581
|
{ ...mapId(e), _partitionKey: nsPartitionValue(ns, e) },
|
|
560
582
|
{
|
|
@@ -564,22 +586,24 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
564
586
|
}
|
|
565
587
|
}
|
|
566
588
|
)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
)
|
|
570
592
|
.pipe(
|
|
571
593
|
Effect
|
|
572
|
-
.flatMap((x) => {
|
|
594
|
+
.flatMap((x): Effect.Effect<PM, OptimisticConcurrencyException | DatabaseError> => {
|
|
573
595
|
if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
|
|
574
596
|
return Effect.fail(
|
|
575
597
|
new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
|
|
576
598
|
)
|
|
577
599
|
}
|
|
578
600
|
if (x.statusCode > 299 || x.statusCode < 200) {
|
|
579
|
-
return Effect.
|
|
580
|
-
new
|
|
581
|
-
"not able to update record: " + x.statusCode
|
|
582
|
-
|
|
601
|
+
return Effect.fail(
|
|
602
|
+
new DatabaseError({
|
|
603
|
+
message: "not able to update record: " + x.statusCode,
|
|
604
|
+
transient: statusIsTransient(x.statusCode),
|
|
605
|
+
cause: x
|
|
606
|
+
})
|
|
583
607
|
)
|
|
584
608
|
}
|
|
585
609
|
return Effect.sync(() => ({
|
package/src/Store/Disk.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import * as fu from "../fileUtil.
|
|
2
|
+
import * as fu from "../fileUtil.ts"
|
|
3
3
|
|
|
4
4
|
import fs from "fs"
|
|
5
5
|
|
|
@@ -9,8 +9,8 @@ import { type PersistenceModelType, type StorageConfig, type Store, type StoreCo
|
|
|
9
9
|
import * as Console from "effect/Console"
|
|
10
10
|
import { flow } from "effect/Function"
|
|
11
11
|
import * as Semaphore from "effect/Semaphore"
|
|
12
|
-
import { annotateDb } from "../otel.
|
|
13
|
-
import { makeMemoryStoreInt } from "./Memory.
|
|
12
|
+
import { annotateDb } from "../otel.ts"
|
|
13
|
+
import { makeMemoryStoreInt } from "./Memory.ts"
|
|
14
14
|
|
|
15
15
|
function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValues, R, E>(
|
|
16
16
|
prefix: string,
|
|
@@ -175,7 +175,9 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
|
|
|
175
175
|
seed?: Effect.Effect<Iterable<Encoded>, E, R>,
|
|
176
176
|
config?: StoreConfig<Encoded>
|
|
177
177
|
) {
|
|
178
|
-
const primary = yield* makeDiskStoreInt(prefix, idKey, "primary", dir, name, seed, config?.defaultValues)
|
|
178
|
+
const primary = yield* makeDiskStoreInt(prefix, idKey, "primary", dir, name, seed, config?.defaultValues).pipe(
|
|
179
|
+
Effect.orDie
|
|
180
|
+
)
|
|
179
181
|
const stores = new Map<string, Store<IdKey, Encoded>>([["primary", primary]])
|
|
180
182
|
const ctx = yield* Effect.context<R>()
|
|
181
183
|
const semaphores = new Map<string, Semaphore.Semaphore>()
|
package/src/Store/Memory.ts
CHANGED
|
@@ -15,15 +15,12 @@ import * as Ref from "effect/Ref"
|
|
|
15
15
|
import * as Result from "effect/Result"
|
|
16
16
|
import * as Semaphore from "effect/Semaphore"
|
|
17
17
|
import * as Struct from "effect/Struct"
|
|
18
|
-
import { InfraLogger } from "../logger.
|
|
19
|
-
import { annotateDb } from "../otel.
|
|
20
|
-
import { codeFilter, codeFilter3_ } from "./codeFilter.
|
|
21
|
-
import { makeUpdateETag } from "./utils.
|
|
18
|
+
import { InfraLogger } from "../logger.ts"
|
|
19
|
+
import { annotateDb } from "../otel.ts"
|
|
20
|
+
import { codeFilter, codeFilter3_ } from "./codeFilter.ts"
|
|
21
|
+
import { get, makeUpdateETag } from "./utils.ts"
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
export function get(obj: any, path: string): any {
|
|
25
|
-
return path.split(".").reduce((res: any, key: string) => (res != null ? res[key] : res), obj)
|
|
26
|
-
}
|
|
23
|
+
export { get } from "./utils.ts"
|
|
27
24
|
|
|
28
25
|
const stripRelationFilterPaths = (state: readonly FilterResult[], relationPath: string): readonly FilterResult[] => {
|
|
29
26
|
const prefix = `${relationPath}.-1.`
|
package/src/Store/SQL/Pg.ts
CHANGED
|
@@ -9,11 +9,11 @@ import * as Option from "effect-app/Option"
|
|
|
9
9
|
import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, storeId, StoreMaker } from "effect-app/Store"
|
|
10
10
|
import * as Struct from "effect/Struct"
|
|
11
11
|
import { SqlClient } from "effect/unstable/sql"
|
|
12
|
-
import { OptimisticConcurrencyException } from "../../errors.
|
|
13
|
-
import { InfraLogger } from "../../logger.
|
|
14
|
-
import { annotateDb } from "../../otel.
|
|
15
|
-
import { makeETag } from "../utils.
|
|
16
|
-
import { buildWhereSQLQuery, logQuery, pgDialect } from "./query.
|
|
12
|
+
import { OptimisticConcurrencyException } from "../../errors.ts"
|
|
13
|
+
import { InfraLogger } from "../../logger.ts"
|
|
14
|
+
import { annotateDb } from "../../otel.ts"
|
|
15
|
+
import { makeETag } from "../utils.ts"
|
|
16
|
+
import { buildWhereSQLQuery, logQuery, pgDialect } from "./query.ts"
|
|
17
17
|
|
|
18
18
|
const parseRow = <Encoded extends FieldValues>(
|
|
19
19
|
row: { id: string; _etag: string | null; data: unknown },
|
package/src/Store/SQL/query.ts
CHANGED
|
@@ -4,8 +4,8 @@ import * as Effect from "effect-app/Effect"
|
|
|
4
4
|
import type { FilterR, FilterResult } from "effect-app/Model/filter/filterApi"
|
|
5
5
|
import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "effect-app/Model/query"
|
|
6
6
|
import { assertUnreachable } from "effect-app/utils"
|
|
7
|
-
import { InfraLogger } from "../../logger.
|
|
8
|
-
import { isRelationCheck } from "../codeFilter.
|
|
7
|
+
import { InfraLogger } from "../../logger.ts"
|
|
8
|
+
import { isRelationCheck } from "../codeFilter.ts"
|
|
9
9
|
|
|
10
10
|
export interface SQLDialect {
|
|
11
11
|
readonly jsonExtract: (path: string) => string
|
package/src/Store/SQL.ts
CHANGED
|
@@ -12,11 +12,19 @@ import * as Layer from "effect/Layer"
|
|
|
12
12
|
import * as LayerMap from "effect/LayerMap"
|
|
13
13
|
import * as Struct from "effect/Struct"
|
|
14
14
|
import { SqlClient } from "effect/unstable/sql"
|
|
15
|
-
import { OptimisticConcurrencyException } from "../errors.
|
|
16
|
-
import { InfraLogger } from "../logger.
|
|
17
|
-
import { annotateDb, type DbSystem } from "../otel.
|
|
18
|
-
import { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.
|
|
19
|
-
import { makeETag } from "./utils.
|
|
15
|
+
import { DatabaseError, OptimisticConcurrencyException } from "../errors.ts"
|
|
16
|
+
import { InfraLogger } from "../logger.ts"
|
|
17
|
+
import { annotateDb, type DbSystem } from "../otel.ts"
|
|
18
|
+
import { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.ts"
|
|
19
|
+
import { makeETag } from "./utils.ts"
|
|
20
|
+
|
|
21
|
+
const sqlErrorMessage = (e: unknown) => (e as any)?.message ? String((e as any).message) : String(e)
|
|
22
|
+
const sqlIsTransient = (e: unknown) =>
|
|
23
|
+
/timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|connection|deadlock|too many connections/i.test(sqlErrorMessage(e))
|
|
24
|
+
// Map a SqlError into a typed, serializable DatabaseError (instead of `.orDie`,
|
|
25
|
+
// which would turn it into an opaque, non-serializable defect).
|
|
26
|
+
const toDatabaseError = (e: unknown) =>
|
|
27
|
+
new DatabaseError({ message: `SQL request failed: ${sqlErrorMessage(e)}`, transient: sqlIsTransient(e), cause: e })
|
|
20
28
|
|
|
21
29
|
export type WithNsTransactionFn = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
|
|
22
30
|
|
|
@@ -101,7 +109,8 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
|
|
|
101
109
|
return { id, _etag: newE._etag!, data, item: newE }
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
const exec = (query: string, params?: readonly unknown[]) =>
|
|
112
|
+
const exec = (query: string, params?: readonly unknown[]) =>
|
|
113
|
+
sql.unsafe(query, params as any).pipe(Effect.mapError(toDatabaseError))
|
|
105
114
|
|
|
106
115
|
const setInternal = Effect.fnUntraced(function*(e: PM, ns: string) {
|
|
107
116
|
const row = toRow(e)
|
|
@@ -151,7 +160,7 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
|
|
|
151
160
|
)
|
|
152
161
|
|
|
153
162
|
const ctx = yield* Effect.context<R>()
|
|
154
|
-
const seedCache = new Map<string, Effect.Effect<void>>()
|
|
163
|
+
const seedCache = new Map<string, Effect.Effect<void, DatabaseError>>()
|
|
155
164
|
const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
|
|
156
165
|
yield* ensureTable
|
|
157
166
|
if (!seed) return
|
|
@@ -365,8 +374,10 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
|
|
|
365
374
|
)
|
|
366
375
|
}
|
|
367
376
|
|
|
368
|
-
// Eagerly seed primary namespace on initialization
|
|
369
|
-
|
|
377
|
+
// Eagerly seed primary namespace on initialization. A seed failure at
|
|
378
|
+
// construction is fatal (orDie); the per-call `seedNamespace` still
|
|
379
|
+
// surfaces DatabaseError to callers.
|
|
380
|
+
yield* seedNamespace("primary").pipe(Effect.orDie)
|
|
370
381
|
|
|
371
382
|
return s
|
|
372
383
|
})
|
|
@@ -412,7 +423,7 @@ function makeSQLiteStorePerNs(
|
|
|
412
423
|
}
|
|
413
424
|
|
|
414
425
|
const exec = (ns: string, query: string, params?: readonly unknown[]) =>
|
|
415
|
-
withNsSql(ns, (sql) => sql.unsafe(query, params as any).pipe(Effect.
|
|
426
|
+
withNsSql(ns, (sql) => sql.unsafe(query, params as any).pipe(Effect.mapError(toDatabaseError)))
|
|
416
427
|
|
|
417
428
|
const ensureTable = (ns: string) =>
|
|
418
429
|
withNsSql(ns, (sql) =>
|
|
@@ -482,7 +493,7 @@ function makeSQLiteStorePerNs(
|
|
|
482
493
|
))
|
|
483
494
|
|
|
484
495
|
const ctx = yield* Effect.context<R>()
|
|
485
|
-
const seedCache = new Map<string, Effect.Effect<void>>()
|
|
496
|
+
const seedCache = new Map<string, Effect.Effect<void, DatabaseError>>()
|
|
486
497
|
const makeSeedEffect = Effect.fnUntraced(function*(ns: string) {
|
|
487
498
|
yield* ensureTable(ns)
|
|
488
499
|
if (!seed) return
|
|
@@ -696,7 +707,7 @@ function makeSQLiteStorePerNs(
|
|
|
696
707
|
)
|
|
697
708
|
}
|
|
698
709
|
|
|
699
|
-
yield* seedNamespace("primary")
|
|
710
|
+
yield* seedNamespace("primary").pipe(Effect.orDie)
|
|
700
711
|
|
|
701
712
|
return s
|
|
702
713
|
})
|
package/src/Store/codeFilter.ts
CHANGED
|
@@ -6,8 +6,7 @@ import type { FieldValues } from "effect-app/Model/filter/types"
|
|
|
6
6
|
import * as Option from "effect-app/Option"
|
|
7
7
|
import type { Filter } from "effect-app/Store"
|
|
8
8
|
import { assertUnreachable } from "effect-app/utils"
|
|
9
|
-
import { get } from "./
|
|
10
|
-
import { compare, greaterThan, greaterThanExclusive, lowerThan, lowerThanExclusive } from "./utils.js"
|
|
9
|
+
import { compare, get, greaterThan, greaterThanExclusive, lowerThan, lowerThanExclusive } from "./utils.ts"
|
|
11
10
|
|
|
12
11
|
const vAsArr = (v: string) => v as unknown as any[]
|
|
13
12
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import * as Test from "@effect-ts/jest/Test"
|
|
3
3
|
import { describe, expect } from "@jest/globals"
|
|
4
4
|
|
|
5
|
-
import { OptimisticConcurrencyException } from "../../errors.
|
|
5
|
+
import { OptimisticConcurrencyException } from "../../errors.ts"
|
|
6
6
|
|
|
7
|
-
import { makeMemoryStore } from "./Memory.
|
|
7
|
+
import { makeMemoryStore } from "./Memory.ts"
|
|
8
8
|
|
|
9
9
|
const { it } = Test.runtime()
|
|
10
10
|
|
package/src/Store/index.ts
CHANGED
|
@@ -5,12 +5,12 @@ import { RepositoryRegistryLive } from "effect-app/Model"
|
|
|
5
5
|
import type { StorageConfig } from "effect-app/Store"
|
|
6
6
|
import * as Redacted from "effect/Redacted"
|
|
7
7
|
import type { SqlClient } from "effect/unstable/sql"
|
|
8
|
-
import { CosmosStoreLayer } from "./Cosmos.
|
|
9
|
-
import { DiskStoreLayer } from "./Disk.
|
|
10
|
-
import { MemoryStoreLive } from "./Memory.
|
|
11
|
-
// import { RedisStoreLayer } from "./Redis.
|
|
12
|
-
import { SQLiteStoreLayer } from "./SQL.
|
|
13
|
-
import { PgStoreLayer } from "./SQL/Pg.
|
|
8
|
+
import { CosmosStoreLayer } from "./Cosmos.ts"
|
|
9
|
+
import { DiskStoreLayer } from "./Disk.ts"
|
|
10
|
+
import { MemoryStoreLive } from "./Memory.ts"
|
|
11
|
+
// import { RedisStoreLayer } from "./Redis.ts"
|
|
12
|
+
import { SQLiteStoreLayer } from "./SQL.ts"
|
|
13
|
+
import { PgStoreLayer } from "./SQL/Pg.ts"
|
|
14
14
|
|
|
15
15
|
export function StoreMakerLayer(
|
|
16
16
|
cfg: StorageConfig,
|
package/src/Store/utils.ts
CHANGED
|
@@ -2,7 +2,12 @@ import crypto from "crypto"
|
|
|
2
2
|
import * as Effect from "effect-app/Effect"
|
|
3
3
|
import * as Option from "effect-app/Option"
|
|
4
4
|
import type { PersistenceModelType, SupportedValues2 } from "effect-app/Store"
|
|
5
|
-
import { OptimisticConcurrencyException } from "../errors.
|
|
5
|
+
import { OptimisticConcurrencyException } from "../errors.ts"
|
|
6
|
+
|
|
7
|
+
/** Traverse an object by a dot-separated path string, e.g. `"a.b.c"`. */
|
|
8
|
+
export function get(obj: any, path: string): any {
|
|
9
|
+
return path.split(".").reduce((res: any, key: string) => (res != null ? res[key] : res), obj)
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
export const makeETag = <E extends PersistenceModelType<{}>>(
|
|
8
13
|
{ _etag, ...e }: E
|
|
@@ -45,9 +45,9 @@ import type * as Scope from "effect/Scope"
|
|
|
45
45
|
import * as Workflow from "effect/unstable/workflow/Workflow"
|
|
46
46
|
import { type Encoded, makeUnsafe, WorkflowEngine, WorkflowInstance } from "effect/unstable/workflow/WorkflowEngine"
|
|
47
47
|
import { randomUUID } from "node:crypto"
|
|
48
|
-
import { CosmosClient, CosmosClientLayer } from "./cosmos-client.
|
|
49
|
-
import { OptimisticConcurrencyException } from "./errors.
|
|
50
|
-
import { annotateCosmosResponse, annotateDb } from "./otel.
|
|
48
|
+
import { CosmosClient, CosmosClientLayer } from "./cosmos-client.ts"
|
|
49
|
+
import { OptimisticConcurrencyException } from "./errors.ts"
|
|
50
|
+
import { annotateCosmosResponse, annotateDb } from "./otel.ts"
|
|
51
51
|
|
|
52
52
|
export interface WorkflowEngineCosmosConfig {
|
|
53
53
|
readonly url: Redacted.Redacted<string>
|