@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/ClusterCosmos.d.ts +16 -16
  3. package/dist/Store/Cosmos.d.ts +1 -1
  4. package/dist/Store/Cosmos.d.ts.map +1 -1
  5. package/dist/Store/Cosmos.js +64 -31
  6. package/dist/Store/Disk.d.ts +2 -2
  7. package/dist/Store/Disk.d.ts.map +1 -1
  8. package/dist/Store/Disk.js +2 -2
  9. package/dist/Store/Memory.d.ts +2 -3
  10. package/dist/Store/Memory.d.ts.map +1 -1
  11. package/dist/Store/Memory.js +3 -6
  12. package/dist/Store/SQL.d.ts +1 -1
  13. package/dist/Store/SQL.d.ts.map +1 -1
  14. package/dist/Store/SQL.js +14 -7
  15. package/dist/Store/codeFilter.d.ts +1 -1
  16. package/dist/Store/codeFilter.d.ts.map +1 -1
  17. package/dist/Store/codeFilter.js +2 -3
  18. package/dist/Store/index.js +1 -1
  19. package/dist/Store/utils.d.ts +3 -1
  20. package/dist/Store/utils.d.ts.map +1 -1
  21. package/dist/Store/utils.js +5 -1
  22. package/dist/WorkflowEngineCosmos.d.ts +1 -1
  23. package/dist/routing/middleware.d.ts +2 -2
  24. package/package.json +3 -3
  25. package/src/CUPS.ts +1 -1
  26. package/src/ClusterCosmos.ts +2 -2
  27. package/src/ContextProvider.ts +1 -1
  28. package/src/Emailer/Sendgrid.ts +1 -1
  29. package/src/Emailer/fake.ts +1 -1
  30. package/src/MainFiberSet.ts +3 -3
  31. package/src/QueueMaker/SQLQueue.ts +3 -3
  32. package/src/QueueMaker/memQueue.ts +4 -4
  33. package/src/QueueMaker/sbqueue.ts +4 -4
  34. package/src/RequestFiberSet.ts +2 -2
  35. package/src/SQL/Model.ts +1 -1
  36. package/src/SQL.ts +1 -1
  37. package/src/ServiceBus.ts +1 -1
  38. package/src/Store/Cosmos/query.ts +2 -2
  39. package/src/Store/Cosmos.ts +88 -64
  40. package/src/Store/Disk.ts +6 -4
  41. package/src/Store/Memory.ts +5 -8
  42. package/src/Store/SQL/Pg.ts +5 -5
  43. package/src/Store/SQL/query.ts +2 -2
  44. package/src/Store/SQL.ts +23 -12
  45. package/src/Store/codeFilter.ts +1 -2
  46. package/src/Store/index.test.ts.bak +2 -2
  47. package/src/Store/index.ts +6 -6
  48. package/src/Store/utils.ts +6 -1
  49. package/src/WorkflowEngineCosmos.ts +3 -3
  50. package/src/WorkflowEngineSqlite.ts +2 -2
  51. package/src/errorReporter.ts +2 -2
  52. package/src/index.ts +2 -2
  53. package/src/internal/events.ts +1 -1
  54. package/src/layerUtils.ts +1 -1
  55. package/src/logger/jsonLogger.ts +1 -1
  56. package/src/logger/logFmtLogger.ts +1 -1
  57. package/src/middlewares.ts +4 -4
  58. package/src/reportError.ts +1 -1
  59. package/src/routing/middleware/middleware.ts +3 -3
  60. package/src/routing.ts +3 -3
  61. package/src/test.ts +2 -2
  62. package/src/vitest.ts +1 -1
@@ -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.js"
10
+ import { InfraLogger } from "../logger.ts"
11
11
 
12
12
  export interface SendgridConfig {
13
13
  defaultReplyTo?: EmailData
@@ -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.js"
4
+ import { InfraLogger } from "../logger.ts"
5
5
 
6
6
  const makeFake = InfraLogger
7
7
  .logInfo("FAKE Emailer Service enabled")
@@ -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.js"
8
- import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.js"
9
- import { setRootParentSpan } from "./RequestFiberSet.js"
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.js"
15
- import { messagingSpanArgs } from "../otel.js"
16
- import { SQLModel } from "../SQL.js"
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.js"
13
- import { MemQueue } from "../memQueue.js"
14
- import { messagingSpanArgs } from "../otel.js"
15
- import { reportNonInterruptedFailure, reportNonInterruptedFailureCause } from "./errors.js"
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.js"
12
- import { messagingSpanArgs } from "../otel.js"
13
- import { Receiver, Sender } from "../ServiceBus.js"
14
- import { reportNonInterruptedFailure, reportNonInterruptedFailureCause, reportQueueError } from "./errors.js"
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 },
@@ -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.js"
10
- import { reportRequestError, reportUnknownRequestError } from "./reportError.js"
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.js"
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.js"
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.js"
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.js"
11
- import { isRelationCheck } from "../codeFilter.js"
10
+ import { InfraLogger } from "../../logger.ts"
11
+ import { isRelationCheck } from "../codeFilter.ts"
12
12
 
13
13
  export function logQuery(q: {
14
14
  query: string
@@ -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.js"
18
- import { OptimisticConcurrencyException } from "../errors.js"
19
- import { InfraLogger } from "../logger.js"
20
- import { annotateCosmosResponse, annotateDb } from "../otel.js"
21
- import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.js"
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
- class CosmosDbOperationError {
34
- constructor(readonly message: string, readonly raw?: unknown) {}
35
- } // TODO: Retry operation when running into RU limit.
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
- Effect
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* Effect.die(
240
- new CosmosDbOperationError(
241
- "not able to update records: " + r2.statusCode,
242
- responses
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* Effect.die(
251
- new CosmosDbOperationError(
252
- "not able to update records: " + r3.statusCode,
253
- responses
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 Effect
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* Effect.die(
334
- new CosmosDbOperationError("not able to update record: " + code)
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* Effect.promise(() =>
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
- Effect
388
- .promise(() =>
389
- execBatch(
390
- mutable(ids.map((id) =>
391
- dropUndefinedT({
392
- operationType: "Delete" as const,
393
- id
394
- // don't use this or we get an error that the request and some item partition key dont match - makese no sense
395
- // partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
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* Effect.promise(() =>
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* Effect.promise(() =>
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* Effect.promise(() =>
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* Effect.promise(() =>
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
- Option
545
- .match(
546
- Option
547
- .fromNullishOr(e._etag),
548
- {
549
- onNone: () =>
550
- Effect.promise(() =>
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.die(
580
- new CosmosDbOperationError(
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.js"
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.js"
13
- import { makeMemoryStoreInt } from "./Memory.js"
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>()
@@ -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.js"
19
- import { annotateDb } from "../otel.js"
20
- import { codeFilter, codeFilter3_ } from "./codeFilter.js"
21
- import { makeUpdateETag } from "./utils.js"
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
- /** Traverse an object by a dot-separated path string, e.g. `"a.b.c"`. */
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.`
@@ -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.js"
13
- import { InfraLogger } from "../../logger.js"
14
- import { annotateDb } from "../../otel.js"
15
- import { makeETag } from "../utils.js"
16
- import { buildWhereSQLQuery, logQuery, pgDialect } from "./query.js"
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 },
@@ -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.js"
8
- import { isRelationCheck } from "../codeFilter.js"
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.js"
16
- import { InfraLogger } from "../logger.js"
17
- import { annotateDb, type DbSystem } from "../otel.js"
18
- import { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.js"
19
- import { makeETag } from "./utils.js"
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[]) => sql.unsafe(query, params as any).pipe(Effect.orDie)
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
- yield* seedNamespace("primary")
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.orDie))
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
  })
@@ -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 "./Memory.js"
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.js"
5
+ import { OptimisticConcurrencyException } from "../../errors.ts"
6
6
 
7
- import { makeMemoryStore } from "./Memory.js"
7
+ import { makeMemoryStore } from "./Memory.ts"
8
8
 
9
9
  const { it } = Test.runtime()
10
10
 
@@ -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.js"
9
- import { DiskStoreLayer } from "./Disk.js"
10
- import { MemoryStoreLive } from "./Memory.js"
11
- // import { RedisStoreLayer } from "./Redis.js"
12
- import { SQLiteStoreLayer } from "./SQL.js"
13
- import { PgStoreLayer } from "./SQL/Pg.js"
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,
@@ -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.js"
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.js"
49
- import { OptimisticConcurrencyException } from "./errors.js"
50
- import { annotateCosmosResponse, annotateDb } from "./otel.js"
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>