@lota-sdk/core 0.4.7 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
package/src/db/service.ts CHANGED
@@ -1,34 +1,35 @@
1
- import {
2
- BoundQuery,
3
- RecordId,
4
- ServerError,
5
- StringRecordId,
6
- Surreal,
7
- Table,
8
- and,
9
- createRemoteEngines,
10
- eq,
11
- } from 'surrealdb'
12
- import type { ExprLike, Mutation, SurrealTransaction, Values } from 'surrealdb'
1
+ import { Duration, Effect, Schedule } from 'effect'
2
+ import { BoundQuery, ServerError, Surreal, Table, createRemoteEngines } from 'surrealdb'
3
+ import type { ExprLike, SurrealTransaction, Values } from 'surrealdb'
4
+ import { ZodError } from 'zod'
13
5
  import type { z } from 'zod'
14
6
 
15
7
  import { serverLogger } from '../config/logger'
16
- import { withTimeout } from '../utils/async'
17
- import { isRecord } from '../utils/string'
8
+ import type { AwaitableEffect } from '../effect/awaitable-effect'
9
+ import { toAwaitableEffect } from '../effect/awaitable-effect'
10
+ import { getErrorMessage } from '../utils/errors'
18
11
  import type { RecordIdInput } from './record-id'
19
- import { ensureRecordId, isSurrealRecordIdValue } from './record-id'
12
+ import { ensureRecordId } from './record-id'
13
+ import {
14
+ assertValidIdentifier,
15
+ buildBoundFilterClauses,
16
+ buildFilterExpression,
17
+ configureMutation,
18
+ describeInvalidValue,
19
+ normalizeBoundQuery,
20
+ normalizeCreateTarget,
21
+ normalizeMutationData,
22
+ normalizeQueryRows,
23
+ normalizeRecordIdForTable,
24
+ normalizeSurrealValue,
25
+ normalizeTableValue,
26
+ normalizeTransactionQuery,
27
+ normalizeTransactionRecordId,
28
+ SurrealDBError,
29
+ } from './service-normalization'
30
+ import type { RecordMutation } from './service-normalization'
20
31
  import type { DatabaseTable } from './tables'
21
-
22
- export class SurrealDBError extends Error {
23
- constructor(
24
- message: string,
25
- public readonly query?: string,
26
- options?: ErrorOptions,
27
- ) {
28
- super(message, options)
29
- this.name = 'SurrealDBError'
30
- }
31
- }
32
+ import { isRetriableTransactionConflict } from './transaction-conflict'
32
33
 
33
34
  export interface SurrealDatabaseConfig {
34
35
  url: string
@@ -51,75 +52,88 @@ interface FindManyOptions {
51
52
  orderDir?: 'ASC' | 'DESC'
52
53
  }
53
54
 
54
- type MutationBuilder = {
55
+ type MutationBuilderSource = {
56
+ content: (data: Record<string, unknown>) => MutationBuilderSource
57
+ replace: (data: Record<string, unknown>) => MutationBuilderSource
58
+ merge: (data: Record<string, unknown>) => MutationBuilderSource
59
+ output: (mode: 'after' | 'before') => unknown
60
+ }
61
+
62
+ export type MutationBuilder = {
55
63
  content: (data: Record<string, unknown>) => MutationBuilder
56
64
  replace: (data: Record<string, unknown>) => MutationBuilder
57
65
  merge: (data: Record<string, unknown>) => MutationBuilder
58
- output: (mode: 'after' | 'before') => Promise<unknown>
66
+ output: (mode: 'after' | 'before') => AwaitableEffect<unknown, SurrealDBError>
59
67
  }
60
68
 
61
- type RecordMutation = Extract<Mutation, 'content' | 'replace' | 'merge'>
69
+ type CreateBuilderSource = {
70
+ content: (data: Record<string, unknown>) => CreateBuilderSource
71
+ output: (mode: 'after' | 'before') => unknown
72
+ }
62
73
 
63
74
  export type CreateMutationBuilder = {
64
75
  content: (data: Record<string, unknown>) => CreateMutationBuilder
65
- output: (mode: 'after' | 'before') => Promise<unknown>
76
+ output: (mode: 'after' | 'before') => AwaitableEffect<unknown, SurrealDBError>
66
77
  }
67
78
 
68
79
  export interface DatabaseTransaction {
69
- query: (query: unknown) => Promise<unknown>
80
+ query: (query: unknown) => AwaitableEffect<unknown, SurrealDBError>
70
81
  create: (target: unknown) => CreateMutationBuilder
71
82
  update: (target: unknown) => MutationBuilder
72
- delete: (target: unknown) => Promise<unknown>
73
- relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) => Promise<unknown>
74
- commit: () => Promise<void>
75
- cancel: () => Promise<void>
76
- }
77
-
78
- function configureMutation(
79
- builder: MutationBuilder,
80
- mutation: RecordMutation,
81
- data: Record<string, unknown>,
82
- ): MutationBuilder {
83
- if (mutation === 'content') {
84
- return builder.content(data)
85
- }
86
- if (mutation === 'replace') {
87
- return builder.replace(data)
88
- }
89
- return builder.merge(data)
83
+ delete: (target: unknown) => AwaitableEffect<unknown, SurrealDBError>
84
+ relate: (
85
+ from: unknown,
86
+ edgeTable: unknown,
87
+ to: unknown,
88
+ data?: Values<Record<string, unknown>>,
89
+ ) => AwaitableEffect<unknown, SurrealDBError>
90
+ commit: () => AwaitableEffect<void, SurrealDBError>
91
+ cancel: () => AwaitableEffect<void, SurrealDBError>
90
92
  }
91
93
 
92
94
  const CONNECT_MAX_ATTEMPTS = 5
93
95
  const CONNECT_RETRY_BASE_DELAY_MS = 100
94
- const CONNECT_RETRY_JITTER_MS = 50
95
96
  const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000
96
97
 
98
+ function isRetriableConnectError(error: unknown): boolean {
99
+ if (isRetriableTransactionConflict(error)) {
100
+ return true
101
+ }
102
+
103
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
104
+ return /(timed out connecting to SurrealDB|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|network|socket|connection)/i.test(
105
+ message,
106
+ )
107
+ }
108
+
97
109
  export class SurrealDBService {
98
110
  private client: Surreal | null = null
99
111
  private isConnected = false
100
- private connectPromise: Promise<void> | null = null
101
112
 
102
113
  constructor(
103
114
  private readonly config: SurrealDatabaseConfig,
104
115
  private readonly logger?: SurrealDatabaseLogger,
105
116
  ) {}
106
117
 
107
- private toSurrealError(error: unknown, query?: string): never {
118
+ private toSurrealError(error: unknown, query?: string): SurrealDBError | ZodError {
108
119
  if (error instanceof SurrealDBError) {
109
- throw error
120
+ return error
121
+ }
122
+
123
+ if (error instanceof ZodError) {
124
+ return error
110
125
  }
111
126
 
112
127
  if (error instanceof ServerError) {
113
- throw new SurrealDBError(`${error.name}: ${error.message}`, query, { cause: error })
128
+ return new SurrealDBError({ message: `${error.name}: ${error.message}`, query: query, cause: error })
114
129
  }
115
130
 
116
131
  if (error instanceof Error) {
117
- throw new SurrealDBError(error.message, query, { cause: error })
132
+ return new SurrealDBError({ message: error.message, query: query, cause: error })
118
133
  }
119
134
 
120
- throw new SurrealDBError(String(error), query)
135
+ return new SurrealDBError({ message: String(error), query })
121
136
  }
122
-
123
137
  private isEmbeddedEngine(url: string) {
124
138
  return (
125
139
  url === 'mem://' ||
@@ -130,76 +144,77 @@ export class SurrealDBService {
130
144
  )
131
145
  }
132
146
 
133
- private async getOrCreateClient(): Promise<Surreal> {
147
+ private getOrCreateClient(): Effect.Effect<Surreal, SurrealDBError> {
134
148
  if (this.client) {
135
- return this.client
149
+ return Effect.succeed(this.client)
136
150
  }
137
151
 
138
152
  const codecOptions = { useNativeDates: true }
139
153
 
140
- if (this.isEmbeddedEngine(this.config.url)) {
141
- const { createNodeEngines } = await import('@surrealdb/node')
142
- this.client = new Surreal({
143
- engines: { ...createRemoteEngines(), ...createNodeEngines() } as NonNullable<
144
- ConstructorParameters<typeof Surreal>[0]
145
- >['engines'],
146
- codecOptions,
147
- })
148
- return this.client
149
- }
150
-
151
- this.client = new Surreal({ engines: createRemoteEngines(), codecOptions })
152
- return this.client
153
- }
154
+ const self = this
155
+ return Effect.gen(function* () {
156
+ if (self.client) {
157
+ return self.client
158
+ }
154
159
 
155
- private async resetClient(): Promise<void> {
156
- if (!this.client) {
157
- return
158
- }
160
+ if (self.isEmbeddedEngine(self.config.url)) {
161
+ const { createNodeEngines } = yield* Effect.tryPromise({
162
+ try: () => import('@surrealdb/node'),
163
+ catch: (error) =>
164
+ new SurrealDBError({
165
+ message: `Failed to load embedded SurrealDB engine: ${getErrorMessage(error)}`,
166
+ cause: error,
167
+ }),
168
+ })
169
+ self.client = new Surreal({
170
+ engines: { ...createRemoteEngines(), ...createNodeEngines() } as NonNullable<
171
+ ConstructorParameters<typeof Surreal>[0]
172
+ >['engines'],
173
+ codecOptions,
174
+ })
175
+ return self.client
176
+ }
159
177
 
160
- try {
161
- await this.client.close()
162
- } catch (error) {
163
- serverLogger.warn`Failed to close database client: ${error}`
164
- } finally {
165
- this.client = null
166
- }
178
+ self.client = new Surreal({ engines: createRemoteEngines(), codecOptions })
179
+ return self.client
180
+ })
167
181
  }
168
182
 
169
- private isRetriableConnectConflict(error: unknown): boolean {
170
- if (!(error instanceof Error)) {
171
- return false
172
- }
173
-
174
- const message = error.message.toLowerCase()
175
- return (
176
- message.includes('transaction conflict') ||
177
- message.includes('transaction read conflict') ||
178
- message.includes('read or write conflict') ||
179
- message.includes('write conflict') ||
180
- message.includes('resource busy') ||
181
- message.includes('this transaction can be retried')
183
+ private resetClient(): Effect.Effect<void, never> {
184
+ if (this.client === null) {
185
+ return Effect.void
186
+ }
187
+
188
+ const client = this.client
189
+
190
+ return Effect.tryPromise({
191
+ try: () => client.close(),
192
+ catch: (error) =>
193
+ new SurrealDBError({ message: `Failed to close database client: ${getErrorMessage(error)}`, cause: error }),
194
+ }).pipe(
195
+ Effect.catch((error: SurrealDBError) =>
196
+ Effect.sync(() => {
197
+ serverLogger.warn`Failed to close database client: ${error.message}`
198
+ }),
199
+ ),
200
+ Effect.asVoid,
201
+ Effect.ensuring(
202
+ Effect.sync(() => {
203
+ this.client = null
204
+ }),
205
+ ),
182
206
  )
183
207
  }
184
208
 
185
- async connect(): Promise<void> {
209
+ connect(): AwaitableEffect<void, SurrealDBError> {
186
210
  if (this.isConnected) {
187
- return
188
- }
189
-
190
- if (this.connectPromise) {
191
- await this.connectPromise
192
- return
211
+ return toAwaitableEffect(Effect.void)
193
212
  }
194
213
 
195
- this.connectPromise = (async () => {
196
- let lastError: unknown = null
197
-
198
- for (let attempt = 1; attempt <= CONNECT_MAX_ATTEMPTS; attempt += 1) {
199
- try {
200
- const client = await this.getOrCreateClient()
201
-
202
- await withTimeout(
214
+ const connectEffect = this.getOrCreateClient().pipe(
215
+ Effect.flatMap((client) =>
216
+ Effect.tryPromise({
217
+ try: () =>
203
218
  client.connect(this.config.url, {
204
219
  namespace: this.config.namespace,
205
220
  database: this.config.database,
@@ -207,356 +222,258 @@ export class SurrealDBService {
207
222
  ? undefined
208
223
  : { username: this.config.username ?? '', password: this.config.password ?? '' },
209
224
  }),
210
- CONNECT_ATTEMPT_TIMEOUT_MS,
211
- `SurrealDB connect (${this.config.url})`,
212
- )
213
-
225
+ catch: (error) =>
226
+ new SurrealDBError({
227
+ message: `Failed to connect to SurrealDB (${this.config.url}): ${getErrorMessage(error)}`,
228
+ cause: error,
229
+ }),
230
+ }).pipe(
231
+ Effect.timeout(Duration.millis(CONNECT_ATTEMPT_TIMEOUT_MS)),
232
+ Effect.catchTag('TimeoutError', () =>
233
+ Effect.fail(new SurrealDBError({ message: `Timed out connecting to SurrealDB (${this.config.url})` })),
234
+ ),
235
+ ),
236
+ ),
237
+ Effect.tap(() =>
238
+ Effect.sync(() => {
214
239
  this.isConnected = true
215
240
  this.logger?.info?.('Connected to SurrealDB')
216
- return
217
- } catch (error) {
218
- lastError = error
241
+ }),
242
+ ),
243
+ Effect.tapError(() =>
244
+ Effect.sync(() => {
219
245
  this.isConnected = false
220
- await this.resetClient()
246
+ }).pipe(Effect.andThen(this.resetClient())),
247
+ ),
248
+ Effect.retry({
249
+ times: CONNECT_MAX_ATTEMPTS - 1,
250
+ schedule: Schedule.jittered(Schedule.exponential(Duration.millis(CONNECT_RETRY_BASE_DELAY_MS), 2)),
251
+ while: isRetriableConnectError,
252
+ }),
253
+ Effect.asVoid,
254
+ )
221
255
 
222
- const retriable = this.isRetriableConnectConflict(error)
223
- const hasMoreAttempts = attempt < CONNECT_MAX_ATTEMPTS
224
- if (!retriable || !hasMoreAttempts) {
225
- break
226
- }
256
+ return toAwaitableEffect(connectEffect)
257
+ }
227
258
 
228
- const backoffMs =
229
- CONNECT_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1) + Math.floor(Math.random() * CONNECT_RETRY_JITTER_MS)
230
- this.logger?.warn?.(
231
- `Retrying SurrealDB connect after retryable transaction conflict (${attempt + 1}/${CONNECT_MAX_ATTEMPTS})`,
232
- )
233
- await Bun.sleep(backoffMs)
259
+ disconnect(): AwaitableEffect<void, SurrealDBError> {
260
+ const self = this
261
+ return toAwaitableEffect(
262
+ Effect.gen(function* () {
263
+ if (!self.isConnected) {
264
+ return
234
265
  }
235
- }
236
266
 
237
- return this.toSurrealError(lastError)
238
- })()
267
+ self.isConnected = false
239
268
 
240
- try {
241
- await this.connectPromise
242
- } finally {
243
- this.connectPromise = null
244
- }
245
- }
269
+ const client = self.client
270
+ if (!client) {
271
+ self.client = null
272
+ return
273
+ }
246
274
 
247
- async disconnect(): Promise<void> {
248
- if (this.connectPromise) {
249
- try {
250
- await this.connectPromise
251
- } catch (error) {
252
- this.logger?.warn?.(
253
- `Disconnect observed failed in-flight connect: ${error instanceof Error ? error.message : String(error)}`,
275
+ yield* Effect.tryPromise({
276
+ try: () => client.close(),
277
+ catch: (error) =>
278
+ new SurrealDBError({ message: `Failed to close database client: ${getErrorMessage(error)}`, cause: error }),
279
+ }).pipe(
280
+ Effect.ensuring(
281
+ Effect.sync(() => {
282
+ self.client = null
283
+ }),
284
+ ),
254
285
  )
255
- return
256
- }
257
- }
258
-
259
- if (!this.isConnected) {
260
- return
261
- }
262
-
263
- this.isConnected = false
264
-
265
- try {
266
- await this.client?.close()
267
- } finally {
268
- this.client = null
269
- }
286
+ }),
287
+ )
270
288
  }
271
289
 
272
- private async ensureConnected(): Promise<Surreal> {
273
- if (this.connectPromise) {
274
- await this.connectPromise
275
- }
276
-
277
- if (this.client === null) {
278
- throw new SurrealDBError('Database not connected')
290
+ private ensureConnectedEffect(query?: string): Effect.Effect<Surreal, SurrealDBError, never> {
291
+ if (this.client === null || !this.isConnected) {
292
+ return this.connect().pipe(
293
+ Effect.flatMap(() => this.getOrCreateClient()),
294
+ Effect.mapError((error) =>
295
+ error instanceof SurrealDBError
296
+ ? error
297
+ : new SurrealDBError({ message: 'Database not connected', query, cause: error }),
298
+ ),
299
+ )
279
300
  }
280
301
 
281
- return this.client
302
+ return Effect.succeed(this.client)
282
303
  }
283
304
 
284
305
  private normalizeRecordId(id: unknown, table: DatabaseTable): ReturnType<typeof ensureRecordId> {
285
- try {
286
- const recordId = ensureRecordId(id as RecordIdInput, table)
287
- const resolvedTable = String(recordId.table)
288
- if (resolvedTable !== table) {
289
- throw new SurrealDBError(`Record id table mismatch: expected "${table}" but got "${resolvedTable}"`)
290
- }
291
- return recordId
292
- } catch (error) {
293
- if (error instanceof SurrealDBError) {
294
- throw error
295
- }
296
- if (error instanceof Error) {
297
- throw new SurrealDBError(`Invalid record id for table "${table}": ${error.message}`, undefined, {
298
- cause: error,
299
- })
300
- }
301
- throw new SurrealDBError(`Invalid record id for table "${table}"`)
302
- }
306
+ return normalizeRecordIdForTable(id, table)
303
307
  }
304
308
 
305
309
  private normalizeQueryRows(statement: unknown, schema?: z.ZodTypeAny): unknown[] {
306
- if (Array.isArray(statement)) {
307
- return schema ? statement.map((row) => this.parseSchema(schema, row)) : (statement as unknown[])
308
- }
309
- if (statement === null || statement === undefined) {
310
- return []
311
- }
312
- return schema ? [this.parseSchema(schema, statement)] : [statement]
310
+ return normalizeQueryRows(statement, schema, (schemaValue, value) => this.parseSchema(schemaValue, value))
313
311
  }
314
312
 
315
313
  private normalizeParseValue(value: unknown): unknown {
316
- if (
317
- value instanceof Date ||
318
- value instanceof RecordId ||
319
- value instanceof StringRecordId ||
320
- value instanceof Table
321
- ) {
322
- return value
323
- }
324
-
325
- if (Array.isArray(value)) {
326
- return value.map((entry) => this.normalizeParseValue(entry))
327
- }
328
-
329
- if (isSurrealRecordIdValue(value)) {
330
- return ensureRecordId(value as RecordIdInput)
331
- }
332
-
333
- if (!isRecord(value)) {
334
- return value
335
- }
336
-
337
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, this.normalizeParseValue(entry)]))
314
+ return normalizeSurrealValue(value)
338
315
  }
339
316
 
340
317
  private parseSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> {
341
318
  return schema.parse(this.normalizeParseValue(value))
342
319
  }
343
320
 
344
- private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
345
- const entries = Object.entries(filter)
346
- if (entries.length === 0) {
347
- return undefined
348
- }
349
-
350
- const expressions = entries.map(([key, value]) => eq(key, this.normalizeRuntimeValue(value)))
351
- if (expressions.length === 1) {
352
- return expressions[0]
321
+ private parseOptionalSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> | null {
322
+ if (value === null || value === undefined) {
323
+ return null
353
324
  }
354
325
 
355
- return and(...expressions)
326
+ return this.parseSchema(schema, value)
356
327
  }
357
328
 
358
- private assertValidIdentifier(name: string, context: string): void {
359
- if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(name)) {
360
- throw new SurrealDBError(`Invalid ${context}: "${name}"`)
361
- }
329
+ private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
330
+ return buildFilterExpression(filter)
362
331
  }
363
332
 
364
333
  private buildBoundFilterClauses(filter: Record<string, unknown>): {
365
334
  clause: string
366
335
  bindings: Record<string, unknown>
367
336
  } {
368
- const entries = Object.entries(filter)
369
- if (entries.length === 0) {
370
- throw new SurrealDBError('Expected a non-empty filter')
371
- }
372
-
373
- const bindings: Record<string, unknown> = {}
374
- const clauses = entries.map(([field, value], index) => {
375
- this.assertValidIdentifier(field, 'filter field')
376
-
377
- const bindingKey = `filter_${index}`
378
- bindings[bindingKey] = this.normalizeRuntimeValue(value)
379
- return `${field} = $${bindingKey}`
380
- })
381
-
382
- return { clause: clauses.join(' AND '), bindings }
337
+ return buildBoundFilterClauses(filter)
383
338
  }
384
339
 
385
- private normalizeBoundQuery(query: BoundQuery): BoundQuery {
386
- return new BoundQuery(query.query, this.normalizeBindings(query.bindings))
340
+ private normalizeBoundQuery<T extends unknown[] = unknown[]>(query: BoundQuery<T>): BoundQuery<T> {
341
+ return normalizeBoundQuery(query)
387
342
  }
388
343
 
389
- private normalizeRuntimeValue(value: unknown): unknown {
390
- if (value === null || value === undefined) {
391
- return value
392
- }
393
-
394
- if (
395
- value instanceof Date ||
396
- value instanceof RecordId ||
397
- value instanceof StringRecordId ||
398
- value instanceof Table
399
- ) {
400
- return value
401
- }
402
-
403
- if (Array.isArray(value)) {
404
- return value.map((entry) => this.normalizeRuntimeValue(entry))
405
- }
406
-
407
- if (isSurrealRecordIdValue(value)) {
408
- return ensureRecordId(value as RecordIdInput)
409
- }
410
-
411
- if (!isRecord(value)) {
412
- return value
413
- }
414
-
415
- if ('tb' in value && 'id' in value && Object.keys(value).length === 2) {
416
- return ensureRecordId(value as unknown as RecordIdInput)
417
- }
418
-
419
- const entries = Object.entries(value)
420
- if (entries.length === 0) {
421
- return value
422
- }
423
-
424
- return Object.fromEntries(entries.map(([key, entryValue]) => [key, this.normalizeRuntimeValue(entryValue)]))
425
- }
426
-
427
- // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
428
- // (non-array objects are mapped entry-by-entry and returned as Object.fromEntries)
429
- private normalizeBindings(bindings?: Record<string, unknown>): Record<string, unknown> | undefined {
430
- if (!bindings) {
431
- return undefined
432
- }
433
-
434
- return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
435
- }
436
-
437
- private normalizeMutationFieldValue(value: unknown): unknown {
438
- if (value === undefined) {
439
- return undefined
440
- }
441
-
442
- if (value === null) {
443
- return null
444
- }
445
-
446
- return this.normalizeRuntimeValue(value)
344
+ private normalizeTransactionQuery(query: unknown): BoundQuery {
345
+ return normalizeTransactionQuery(query)
447
346
  }
448
347
 
449
- // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
450
348
  private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
451
- return Object.fromEntries(
452
- Object.entries(data)
453
- .map(([key, value]) => [key, this.normalizeMutationFieldValue(value)] as const)
454
- .filter((entry): entry is readonly [string, unknown] => entry[1] !== undefined),
455
- ) as Record<string, unknown>
456
- }
457
-
458
- private normalizeTableValue(value: unknown): Table {
459
- if (value instanceof Table) {
460
- return value as Table<string>
461
- }
462
-
463
- if (typeof value === 'string' && value.length > 0) {
464
- return new Table(value)
465
- }
466
-
467
- throw new SurrealDBError('Invalid table value')
349
+ return normalizeMutationData(data)
468
350
  }
469
351
 
470
- private isRecordIdLike(value: unknown): boolean {
471
- if (value instanceof RecordId || value instanceof StringRecordId) {
472
- return true
473
- }
474
-
475
- if (isSurrealRecordIdValue(value)) {
476
- return true
477
- }
478
-
479
- if (typeof value === 'string') {
480
- return /^[a-zA-Z][a-zA-Z0-9_]*:/.test(value)
481
- }
482
-
483
- if (value && typeof value === 'object') {
484
- const record = value as { tb?: unknown; id?: unknown }
485
- return typeof record.tb === 'string' && record.id !== undefined && Object.keys(value).length === 2
486
- }
487
-
488
- return false
352
+ private normalizeTableValue(value: unknown): Table<string> {
353
+ return normalizeTableValue(value)
489
354
  }
490
355
 
491
- private normalizeCreateTarget(value: unknown): Table | RecordId {
492
- if (this.isRecordIdLike(value)) {
493
- return ensureRecordId(value as RecordIdInput)
494
- }
495
-
496
- return this.normalizeTableValue(value)
356
+ private normalizeCreateTarget(value: unknown): Table<string> | ReturnType<typeof ensureRecordId> {
357
+ return normalizeCreateTarget(value)
497
358
  }
498
359
 
499
- private wrapMutationBuilder(builder: MutationBuilder): MutationBuilder {
360
+ private wrapMutationBuilder(builder: MutationBuilderSource): MutationBuilder {
500
361
  return {
501
362
  content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
502
363
  replace: (data) => this.wrapMutationBuilder(builder.replace(this.normalizeMutationData(data))),
503
364
  merge: (data) => this.wrapMutationBuilder(builder.merge(this.normalizeMutationData(data))),
504
- output: async (mode) => this.normalizeParseValue(await builder.output(mode)),
365
+ output: (mode) =>
366
+ toAwaitableEffect(
367
+ Effect.tryPromise({
368
+ try: () => Promise.resolve(builder.output(mode)),
369
+ catch: (error) =>
370
+ new SurrealDBError({
371
+ message: `Failed to finish mutation output: ${getErrorMessage(error)}`,
372
+ cause: error,
373
+ }),
374
+ }).pipe(Effect.map((value) => this.normalizeParseValue(value))),
375
+ ),
505
376
  }
506
377
  }
507
378
 
508
- private wrapCreateBuilder(builder: CreateMutationBuilder): CreateMutationBuilder {
379
+ private wrapCreateBuilder(builder: CreateBuilderSource): CreateMutationBuilder {
509
380
  return {
510
381
  content: (data) => this.wrapCreateBuilder(builder.content(this.normalizeMutationData(data))),
511
- output: async (mode) => this.normalizeParseValue(await builder.output(mode)),
382
+ output: (mode) =>
383
+ toAwaitableEffect(
384
+ Effect.tryPromise({
385
+ try: () => Promise.resolve(builder.output(mode)),
386
+ catch: (error) =>
387
+ new SurrealDBError({
388
+ message: `Failed to finish create output: ${getErrorMessage(error)}`,
389
+ cause: error,
390
+ }),
391
+ }).pipe(Effect.map((value) => this.normalizeParseValue(value))),
392
+ ),
512
393
  }
513
394
  }
514
395
 
515
396
  private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
397
+ const self = this
516
398
  return {
517
- query: async (query: unknown) => {
518
- const boundQuery = this.normalizeBoundQuery(query as BoundQuery)
519
- const queryText = this.resolveQueryText(boundQuery)
520
-
521
- try {
522
- const responses = await tx.query(boundQuery).responses()
523
- const first = responses.at(0)
524
- if (!first) {
525
- return []
526
- }
527
- if (!first.success) {
528
- throw new SurrealDBError(first.error.message, queryText, { cause: first.error })
529
- }
530
-
531
- return this.normalizeQueryRows(first.result)
532
- } catch (error) {
533
- return this.toSurrealError(error, queryText)
534
- }
399
+ query: (query: unknown) => {
400
+ const boundQuery = self.normalizeTransactionQuery(query)
401
+ const queryText = self.resolveQueryText(boundQuery)
402
+ return toAwaitableEffect(
403
+ Effect.gen(function* () {
404
+ const responses = yield* Effect.tryPromise({
405
+ try: () => tx.query(boundQuery).responses(),
406
+ catch: (error) =>
407
+ new SurrealDBError({
408
+ message: `Failed to run transaction query: ${getErrorMessage(error)}`,
409
+ query: queryText,
410
+ cause: error,
411
+ }),
412
+ })
413
+ const first = responses.at(0)
414
+ if (!first) {
415
+ return []
416
+ }
417
+ if (!first.success) {
418
+ return yield* new SurrealDBError({ message: first.error.message, query: queryText, cause: first.error })
419
+ }
420
+
421
+ return self.normalizeQueryRows(first.result)
422
+ }),
423
+ )
535
424
  },
536
425
  create: (target: unknown) => {
537
- const normalizedTarget = this.normalizeCreateTarget(target)
538
- const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
539
- // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal CreateMutationBuilder interface
540
- return this.wrapCreateBuilder(builder as unknown as CreateMutationBuilder)
426
+ const normalizedTarget = self.normalizeCreateTarget(target)
427
+ const builder: CreateBuilderSource =
428
+ normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
429
+ return self.wrapCreateBuilder(builder)
541
430
  },
542
431
  update: (target: unknown) =>
543
- // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal MutationBuilder interface
544
- this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
545
- delete: async (target: unknown) =>
546
- this.normalizeParseValue(await tx.delete(ensureRecordId(target as RecordIdInput))),
547
- relate: async (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
548
- this.normalizeParseValue(
549
- await tx.relate(
550
- ensureRecordId(from as RecordIdInput),
551
- this.normalizeTableValue(edgeTable),
552
- ensureRecordId(to as RecordIdInput),
553
- data
554
- ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<Record<string, unknown>>)
555
- : undefined,
556
- ),
432
+ self.wrapMutationBuilder(tx.update(normalizeTransactionRecordId(target, 'transaction update'))),
433
+ delete: (target: unknown) =>
434
+ toAwaitableEffect(
435
+ Effect.tryPromise({
436
+ try: () => tx.delete(normalizeTransactionRecordId(target, 'transaction delete')),
437
+ catch: (error) =>
438
+ new SurrealDBError({
439
+ message: `Failed to delete transaction target: ${getErrorMessage(error)}`,
440
+ cause: error,
441
+ }),
442
+ }).pipe(Effect.map((value) => this.normalizeParseValue(value))),
443
+ ),
444
+ relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
445
+ toAwaitableEffect(
446
+ Effect.tryPromise({
447
+ try: () =>
448
+ tx.relate(
449
+ normalizeTransactionRecordId(from, 'transaction relate source'),
450
+ self.normalizeTableValue(edgeTable),
451
+ normalizeTransactionRecordId(to, 'transaction relate target'),
452
+ data ? self.normalizeMutationData(data as Record<string, unknown>) : undefined,
453
+ ),
454
+ catch: (error) =>
455
+ new SurrealDBError({
456
+ message: `Failed to relate transaction records: ${getErrorMessage(error)}`,
457
+ cause: error,
458
+ }),
459
+ }).pipe(Effect.map((value) => this.normalizeParseValue(value))),
460
+ ),
461
+ commit: () =>
462
+ toAwaitableEffect(
463
+ Effect.tryPromise({
464
+ try: () => tx.commit(),
465
+ catch: (error) =>
466
+ new SurrealDBError({ message: `Failed to commit transaction: ${getErrorMessage(error)}`, cause: error }),
467
+ }),
468
+ ),
469
+ cancel: () =>
470
+ toAwaitableEffect(
471
+ Effect.tryPromise({
472
+ try: () => tx.cancel(),
473
+ catch: (error) =>
474
+ new SurrealDBError({ message: `Failed to cancel transaction: ${getErrorMessage(error)}`, cause: error }),
475
+ }),
557
476
  ),
558
- commit: () => tx.commit(),
559
- cancel: () => tx.cancel(),
560
477
  }
561
478
  }
562
479
 
@@ -564,459 +481,507 @@ export class SurrealDBService {
564
481
  return query.query
565
482
  }
566
483
 
567
- async query<T>(query: BoundQuery): Promise<T[]> {
568
- const statements = await this.queryAll<T>(query)
569
- return statements.at(0) ?? []
484
+ query<T>(query: BoundQuery): AwaitableEffect<T[], SurrealDBError> {
485
+ const self = this
486
+ return toAwaitableEffect(
487
+ Effect.gen(function* () {
488
+ const boundQuery = self.normalizeBoundQuery(query)
489
+ const statements = yield* self.queryAll<T>(boundQuery)
490
+ return statements.at(0) ?? []
491
+ }),
492
+ )
570
493
  }
571
494
 
572
- async queryAll<T>(query: BoundQuery, schema?: z.ZodTypeAny): Promise<T[][]> {
573
- const client = await this.ensureConnected()
574
- const boundQuery = this.normalizeBoundQuery(query)
575
- const queryText = this.resolveQueryText(boundQuery)
576
-
577
- try {
578
- const responses = await client.query(boundQuery).responses()
579
-
580
- return responses.map((response, index) => {
581
- if (!response.success) {
582
- const failure = response.error
583
- throw new SurrealDBError(`Statement ${index + 1}: ${failure.message}`, queryText, { cause: failure })
584
- }
585
- return this.normalizeQueryRows(response.result, schema) as T[]
586
- })
587
- } catch (error) {
588
- return this.toSurrealError(error, queryText)
589
- }
495
+ queryAll<T>(query: BoundQuery, schema?: z.ZodTypeAny): AwaitableEffect<T[][], SurrealDBError> {
496
+ const self = this
497
+ return toAwaitableEffect(
498
+ Effect.gen(function* () {
499
+ const boundQuery = self.normalizeBoundQuery(query)
500
+ const queryText = self.resolveQueryText(boundQuery)
501
+ const client = yield* self.ensureConnectedEffect(queryText)
502
+ const responses = yield* Effect.tryPromise({
503
+ try: () => client.query(boundQuery).responses(),
504
+ catch: (error) =>
505
+ new SurrealDBError({
506
+ message: `Failed to run query: ${getErrorMessage(error)}`,
507
+ query: queryText,
508
+ cause: error,
509
+ }),
510
+ })
511
+ return yield* Effect.forEach(responses, (response, index) =>
512
+ Effect.gen(function* () {
513
+ if (!response.success) {
514
+ const failure = response.error
515
+ return yield* new SurrealDBError({
516
+ message: `Statement ${index + 1}: ${failure.message}`,
517
+ query: queryText,
518
+ cause: failure,
519
+ })
520
+ }
521
+ return self.normalizeQueryRows(response.result, schema) as T[]
522
+ }),
523
+ )
524
+ }),
525
+ )
590
526
  }
591
527
 
592
- async queryOne<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): Promise<z.infer<T> | null> {
593
- const results = await this.query<unknown>(query)
594
- const first = results.at(0)
595
- return first ? this.parseSchema(schema, first) : null
528
+ queryOne<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
529
+ const self = this
530
+ return toAwaitableEffect(
531
+ Effect.gen(function* () {
532
+ const boundQuery = self.normalizeBoundQuery(query)
533
+ const results = yield* self.query<unknown>(boundQuery)
534
+ const first = results.at(0)
535
+ return first ? self.parseSchema(schema, first) : null
536
+ }),
537
+ )
596
538
  }
597
539
 
598
- async queryMany<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): Promise<z.infer<T>[]> {
599
- const results = await this.query<unknown>(query)
600
- return results.map((row) => this.parseSchema(schema, row))
540
+ queryMany<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): AwaitableEffect<z.infer<T>[], SurrealDBError> {
541
+ const self = this
542
+ return toAwaitableEffect(
543
+ Effect.gen(function* () {
544
+ const boundQuery = self.normalizeBoundQuery(query)
545
+ const results = yield* self.query<unknown>(boundQuery)
546
+ return results.map((row) => self.parseSchema(schema, row))
547
+ }),
548
+ )
601
549
  }
602
550
 
603
- private async runSqlFile(file: Bun.BunFile): Promise<void> {
604
- const sql = (await file.text()).trim()
605
- if (!sql) {
606
- return
607
- }
551
+ private runSqlFile(file: Bun.BunFile): Effect.Effect<void, SurrealDBError> {
552
+ const self = this
553
+ return Effect.gen(function* () {
554
+ const sql = (yield* Effect.tryPromise({
555
+ try: () => file.text(),
556
+ catch: (error) =>
557
+ new SurrealDBError({ message: `Failed to read schema file: ${getErrorMessage(error)}`, cause: error }),
558
+ })).trim()
559
+ if (!sql) {
560
+ return
561
+ }
608
562
 
609
- await this.queryAll<unknown>(new BoundQuery(sql))
563
+ yield* self.queryAll<unknown>(new BoundQuery(sql))
564
+ })
610
565
  }
611
566
 
612
- async applySchema(schemaFiles: readonly Bun.BunFile[]): Promise<void> {
613
- for (const schemaFile of schemaFiles) {
614
- await this.runSqlFile(schemaFile)
615
- }
567
+ applySchema(schemaFiles: readonly Bun.BunFile[]): AwaitableEffect<void, SurrealDBError> {
568
+ return toAwaitableEffect(
569
+ Effect.forEach(schemaFiles, (schemaFile) => this.runSqlFile(schemaFile), { discard: true }),
570
+ )
616
571
  }
617
572
 
618
- async findOne<T extends z.ZodTypeAny>(
573
+ findOne<T extends z.ZodTypeAny>(
619
574
  table: DatabaseTable,
620
575
  filter: Record<string, unknown>,
621
576
  schema: T,
622
- ): Promise<z.infer<T> | null> {
623
- const client = await this.ensureConnected()
624
- const selection = this.buildFilterExpression(filter)
625
-
626
- try {
627
- let query = client.select<unknown>(new Table(table))
628
- if (selection) {
629
- query = query.where(selection)
630
- }
577
+ ): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
578
+ const self = this
579
+ return toAwaitableEffect(
580
+ Effect.gen(function* () {
581
+ const selection = self.buildFilterExpression(filter)
582
+ const client = yield* self.ensureConnectedEffect(`SELECT * FROM ${table} LIMIT 1`)
583
+ let query = client.select<unknown>(new Table(table))
584
+ if (selection) {
585
+ query = query.where(selection)
586
+ }
631
587
 
632
- const rows = await query.limit(1)
633
- const first = rows.at(0)
634
- return first ? this.parseSchema(schema, first) : null
635
- } catch (error) {
636
- return this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
637
- }
588
+ const rows = yield* Effect.tryPromise({
589
+ try: () => query.limit(1),
590
+ catch: (error) =>
591
+ new SurrealDBError({
592
+ message: `Failed to find record in ${table}: ${getErrorMessage(error)}`,
593
+ cause: error,
594
+ }),
595
+ })
596
+ const first = rows.at(0)
597
+ return first ? self.parseSchema(schema, first) : null
598
+ }),
599
+ )
638
600
  }
639
601
 
640
- async findMany<T extends z.ZodTypeAny>(
602
+ findMany<T extends z.ZodTypeAny>(
641
603
  table: DatabaseTable,
642
604
  filter: Record<string, unknown>,
643
605
  schema: T,
644
606
  options?: FindManyOptions,
645
- ): Promise<z.infer<T>[]> {
646
- const filterKeys = Object.keys(filter)
647
-
648
- const client = await this.ensureConnected()
649
- const selection = this.buildFilterExpression(filter)
650
- const orderBy = options?.orderBy
651
-
652
- if (orderBy !== undefined) {
653
- this.assertValidIdentifier(orderBy, 'orderBy field')
654
- this.assertValidIdentifier(table, 'table name')
655
- for (const key of filterKeys) {
656
- this.assertValidIdentifier(key, 'filter field')
657
- }
658
- const rawOrderDir: unknown = options?.orderDir
659
- if (rawOrderDir !== undefined && rawOrderDir !== 'ASC' && rawOrderDir !== 'DESC') {
660
- throw new SurrealDBError(`Invalid orderDir value: ${this.describeInvalidValue(rawOrderDir)}`)
661
- }
662
- const orderDir = rawOrderDir ?? 'ASC'
663
- const limit = options?.limit
664
- const offset = options?.offset
665
- const vars: Record<string, unknown> = this.normalizeMutationData(filter)
666
- let sql = `SELECT * FROM ${table}`
667
- if (filterKeys.length > 0) {
668
- const conditions = filterKeys.map((key) => `${key} = $${key}`).join(' AND ')
669
- sql += ` WHERE ${conditions}`
670
- }
671
- sql += ` ORDER BY ${orderBy} ${orderDir}`
672
- if (limit !== undefined) {
673
- sql += ' LIMIT $limitParam'
674
- vars.limitParam = limit
675
- }
676
- if (offset !== undefined) {
677
- sql += ' START $offsetParam'
678
- vars.offsetParam = offset
679
- }
680
- const rows = await this.query<unknown>(new BoundQuery(sql, vars))
681
- return rows.map((row) => this.parseSchema(schema, row))
682
- }
683
-
684
- try {
685
- let query = client.select<unknown>(new Table(table))
686
- if (selection) {
687
- query = query.where(selection)
688
- }
689
- if (options?.offset !== undefined) {
690
- query = query.start(options.offset)
691
- }
692
- if (options?.limit !== undefined) {
693
- query = query.limit(options.limit)
694
- }
695
-
696
- const rows = await query
697
- return rows.map((row) => this.parseSchema(schema, row))
698
- } catch (error) {
699
- return this.toSurrealError(error, `SELECT * FROM ${table}`)
700
- }
701
- }
607
+ ): AwaitableEffect<z.infer<T>[], SurrealDBError> {
608
+ const self = this
609
+ return toAwaitableEffect(
610
+ Effect.gen(function* () {
611
+ const filterKeys = Object.keys(filter)
612
+ const selection = self.buildFilterExpression(filter)
613
+ const orderBy = options?.orderBy
614
+
615
+ if (orderBy !== undefined) {
616
+ assertValidIdentifier(orderBy, 'orderBy field')
617
+ assertValidIdentifier(table, 'table name')
618
+ for (const key of filterKeys) {
619
+ assertValidIdentifier(key, 'filter field')
620
+ }
621
+ const rawOrderDir: unknown = options?.orderDir
622
+ if (rawOrderDir !== undefined && rawOrderDir !== 'ASC' && rawOrderDir !== 'DESC') {
623
+ return yield* new SurrealDBError({
624
+ message: `Invalid orderDir value: ${describeInvalidValue(rawOrderDir)}`,
625
+ })
626
+ }
627
+ const orderDir = rawOrderDir ?? 'ASC'
628
+ const limit = options?.limit
629
+ const offset = options?.offset
630
+ const vars: Record<string, unknown> = self.normalizeMutationData(filter)
631
+ let sql = `SELECT * FROM ${table}`
632
+ if (filterKeys.length > 0) {
633
+ const conditions = filterKeys.map((key) => `${key} = $${key}`).join(' AND ')
634
+ sql += ` WHERE ${conditions}`
635
+ }
636
+ sql += ` ORDER BY ${orderBy} ${orderDir}`
637
+ if (limit !== undefined) {
638
+ sql += ' LIMIT $limitParam'
639
+ vars.limitParam = limit
640
+ }
641
+ if (offset !== undefined) {
642
+ sql += ' START $offsetParam'
643
+ vars.offsetParam = offset
644
+ }
645
+ const rows = yield* self.query<unknown>(new BoundQuery(sql, vars))
646
+ return rows.map((row) => self.parseSchema(schema, row))
647
+ }
702
648
 
703
- private describeInvalidValue(value: unknown): string {
704
- if (typeof value === 'string') {
705
- return value
706
- }
649
+ const client = yield* self.ensureConnectedEffect(`SELECT * FROM ${table}`)
650
+ let query = client.select<unknown>(new Table(table))
651
+ if (selection) {
652
+ query = query.where(selection)
653
+ }
654
+ if (options?.offset !== undefined) {
655
+ query = query.start(options.offset)
656
+ }
657
+ if (options?.limit !== undefined) {
658
+ query = query.limit(options.limit)
659
+ }
707
660
 
708
- try {
709
- const serialized = JSON.stringify(value)
710
- if (typeof serialized === 'string') {
711
- return serialized
712
- }
713
- return Object.prototype.toString.call(value)
714
- } catch {
715
- return Object.prototype.toString.call(value)
716
- }
661
+ const rows = yield* Effect.tryPromise({
662
+ try: () => query,
663
+ catch: (error) =>
664
+ new SurrealDBError({
665
+ message: `Failed to run findMany query for ${table}: ${getErrorMessage(error)}`,
666
+ cause: error,
667
+ }),
668
+ })
669
+ return rows.map((row) => self.parseSchema(schema, row))
670
+ }),
671
+ )
717
672
  }
718
673
 
719
- async create<T extends z.ZodTypeAny>(
674
+ create<T extends z.ZodTypeAny>(
720
675
  table: DatabaseTable,
721
676
  data: Record<string, unknown>,
722
677
  schema: T,
723
- ): Promise<z.infer<T>> {
724
- const keys = Object.keys(data)
725
- if (keys.length === 0) {
726
- throw new SurrealDBError(`Cannot create record in ${table} with empty data`)
727
- }
728
-
729
- const client = await this.ensureConnected()
678
+ ): AwaitableEffect<z.infer<T>, SurrealDBError> {
679
+ const self = this
680
+ return toAwaitableEffect(
681
+ Effect.gen(function* () {
682
+ const keys = Object.keys(data)
683
+ if (keys.length === 0) {
684
+ return yield* new SurrealDBError({ message: `Cannot create record in ${table} with empty data` })
685
+ }
730
686
 
731
- try {
732
- const created = await client
733
- .create<unknown>(new Table(table))
734
- .content(this.normalizeMutationData(data))
735
- .output('after')
736
- const first = Array.isArray(created) ? created.at(0) : created
687
+ const client = yield* self.ensureConnectedEffect(`CREATE ${table}`)
688
+ const created = yield* Effect.tryPromise({
689
+ try: () => client.create<unknown>(new Table(table)).content(self.normalizeMutationData(data)).output('after'),
690
+ catch: (error) =>
691
+ new SurrealDBError({
692
+ message: `Failed to create record in ${table}: ${getErrorMessage(error)}`,
693
+ cause: error,
694
+ }),
695
+ })
696
+ const first = Array.isArray(created) ? created.at(0) : created
737
697
 
738
- if (!first) {
739
- throw new SurrealDBError(`Failed to create record in ${table}`)
740
- }
698
+ if (!first) {
699
+ return yield* new SurrealDBError({ message: `Failed to create record in ${table}` })
700
+ }
741
701
 
742
- return this.parseSchema(schema, first)
743
- } catch (error) {
744
- return this.toSurrealError(error, `CREATE ${table}`)
745
- }
702
+ return self.parseSchema(schema, first)
703
+ }),
704
+ )
746
705
  }
747
706
 
748
- async createWithId<T extends z.ZodTypeAny>(
707
+ createWithId<T extends z.ZodTypeAny>(
749
708
  table: DatabaseTable,
750
709
  id: unknown,
751
710
  data: Record<string, unknown>,
752
711
  schema: T,
753
- ): Promise<z.infer<T>> {
754
- const keys = Object.keys(data)
755
- if (keys.length === 0) {
756
- throw new SurrealDBError(`Cannot create record in ${table} with empty data`)
757
- }
758
-
759
- const recordId = this.normalizeRecordId(id, table)
760
- const client = await this.ensureConnected()
712
+ ): AwaitableEffect<z.infer<T>, SurrealDBError> {
713
+ const self = this
714
+ return toAwaitableEffect(
715
+ Effect.gen(function* () {
716
+ const recordId = self.normalizeRecordId(id, table)
717
+ const keys = Object.keys(data)
718
+ if (keys.length === 0) {
719
+ return yield* new SurrealDBError({ message: `Cannot create record in ${table} with empty data` })
720
+ }
761
721
 
762
- try {
763
- const created = await client.create<unknown>(recordId).content(this.normalizeMutationData(data)).output('after')
764
- return this.parseSchema(schema, created)
765
- } catch (error) {
766
- return this.toSurrealError(error, `CREATE ${recordId.toString()}`)
767
- }
722
+ const client = yield* self.ensureConnectedEffect(`CREATE ${recordId.toString()}`)
723
+ const created = yield* Effect.tryPromise({
724
+ try: () => client.create<unknown>(recordId).content(self.normalizeMutationData(data)).output('after'),
725
+ catch: (error) =>
726
+ new SurrealDBError({
727
+ message: `Failed to create record in ${table}: ${getErrorMessage(error)}`,
728
+ cause: error,
729
+ }),
730
+ })
731
+ return self.parseSchema(schema, created)
732
+ }),
733
+ )
768
734
  }
769
735
 
770
- async update<T extends z.ZodTypeAny>(
736
+ update<T extends z.ZodTypeAny>(
771
737
  table: DatabaseTable,
772
738
  id: unknown,
773
739
  data: Record<string, unknown>,
774
740
  schema: T,
775
741
  options?: { mutation?: RecordMutation },
776
- ): Promise<z.infer<T> | null> {
777
- const recordId = this.normalizeRecordId(id, table)
778
-
779
- const keys = Object.keys(data)
780
- if (keys.length === 0) {
781
- throw new SurrealDBError('Cannot update record with empty data')
782
- }
783
-
784
- const client = await this.ensureConnected()
785
- const mutation = options?.mutation ?? 'merge'
742
+ ): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
743
+ const self = this
744
+ return toAwaitableEffect(
745
+ Effect.gen(function* () {
746
+ const recordId = self.normalizeRecordId(id, table)
747
+ const keys = Object.keys(data)
748
+ if (keys.length === 0) {
749
+ return yield* new SurrealDBError({ message: 'Cannot update record with empty data' })
750
+ }
786
751
 
787
- try {
788
- const builder = client.update<unknown>(recordId)
789
- const configured = configureMutation(builder, mutation, this.normalizeMutationData(data))
790
- const updated = await configured.output('after')
791
- return updated ? this.parseSchema(schema, updated) : null
792
- } catch (error) {
793
- return this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
794
- }
752
+ const mutation = options?.mutation ?? 'merge'
753
+ const client = yield* self.ensureConnectedEffect(`UPDATE ${recordId.toString()}`)
754
+ const builder = client.update<unknown>(recordId)
755
+ const updated: Record<string, unknown> | null | undefined = yield* Effect.tryPromise({
756
+ try: () => configureMutation(builder, mutation, self.normalizeMutationData(data)).output('after'),
757
+ catch: (error) =>
758
+ new SurrealDBError({
759
+ message: `Failed to update record in ${table}: ${getErrorMessage(error)}`,
760
+ cause: error,
761
+ }),
762
+ })
763
+ return self.parseOptionalSchema(schema, updated)
764
+ }),
765
+ )
795
766
  }
796
767
 
797
- async upsert<T extends z.ZodTypeAny>(
768
+ upsert<T extends z.ZodTypeAny>(
798
769
  table: DatabaseTable,
799
770
  id: unknown,
800
771
  data: Record<string, unknown>,
801
772
  schema: T,
802
773
  options?: { mutation?: RecordMutation },
803
- ): Promise<z.infer<T>> {
804
- const recordId = this.normalizeRecordId(id, table)
805
- const keys = Object.keys(data)
806
- if (keys.length === 0) {
807
- throw new SurrealDBError('Cannot upsert record with empty data')
808
- }
809
-
810
- const client = await this.ensureConnected()
811
- const mutation = options?.mutation ?? 'merge'
774
+ ): AwaitableEffect<z.infer<T>, SurrealDBError> {
775
+ const self = this
776
+ return toAwaitableEffect(
777
+ Effect.gen(function* () {
778
+ const recordId = self.normalizeRecordId(id, table)
779
+ const keys = Object.keys(data)
780
+ if (keys.length === 0) {
781
+ return yield* new SurrealDBError({ message: 'Cannot upsert record with empty data' })
782
+ }
812
783
 
813
- try {
814
- const builder = client.upsert<unknown>(recordId)
815
- const configured = configureMutation(builder, mutation, this.normalizeMutationData(data))
816
- const upserted = await configured.output('after')
817
- if (!upserted) {
818
- throw new SurrealDBError(`Failed to upsert record in ${table}`)
819
- }
820
- return this.parseSchema(schema, upserted)
821
- } catch (error) {
822
- return this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
823
- }
784
+ const mutation = options?.mutation ?? 'merge'
785
+ const client = yield* self.ensureConnectedEffect(`UPSERT ${recordId.toString()}`)
786
+ const builder = client.upsert<unknown>(recordId)
787
+ const upserted: Record<string, unknown> | null | undefined = yield* Effect.tryPromise({
788
+ try: () => configureMutation(builder, mutation, self.normalizeMutationData(data)).output('after'),
789
+ catch: (error) =>
790
+ new SurrealDBError({
791
+ message: `Failed to upsert record in ${table}: ${getErrorMessage(error)}`,
792
+ cause: error,
793
+ }),
794
+ })
795
+ const parsed = self.parseOptionalSchema(schema, upserted)
796
+ if (parsed === null) {
797
+ return yield* new SurrealDBError({ message: `Failed to upsert record in ${table}` })
798
+ }
799
+ return parsed
800
+ }),
801
+ )
824
802
  }
825
803
 
826
- async deleteById(table: DatabaseTable, id: unknown): Promise<boolean> {
827
- const recordId = this.normalizeRecordId(id, table)
828
-
829
- try {
830
- const result = await this.query<unknown>(new BoundQuery(`DELETE $recordId RETURN BEFORE`, { recordId }))
831
- return result.length > 0
832
- } catch (error) {
833
- if (error instanceof Error && error.message.includes('does not exist')) {
834
- return false
835
- }
836
- return this.toSurrealError(error, `DELETE ${recordId.toString()}`)
837
- }
804
+ deleteById(table: DatabaseTable, id: unknown): AwaitableEffect<boolean, SurrealDBError> {
805
+ const self = this
806
+ return toAwaitableEffect(
807
+ Effect.gen(function* () {
808
+ const recordId = self.normalizeRecordId(id, table)
809
+ return yield* self.query<unknown>(new BoundQuery(`DELETE $recordId RETURN BEFORE`, { recordId })).pipe(
810
+ Effect.map((result) => result.length > 0),
811
+ Effect.catchTag('SurrealDBError', (error) => {
812
+ if (error.message.includes('does not exist')) {
813
+ return Effect.succeed(false)
814
+ }
815
+
816
+ return Effect.fail(self.toSurrealError(error, `DELETE ${recordId.toString()}`) as SurrealDBError)
817
+ }),
818
+ )
819
+ }),
820
+ )
838
821
  }
839
822
 
840
- async deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): Promise<number> {
841
- this.assertValidIdentifier(table, 'table name')
842
- const filterKeys = Object.keys(filter)
843
- if (filterKeys.length === 0) {
844
- throw new SurrealDBError(`Refusing to delete all records in ${table} without a filter`)
845
- }
846
- const { clause, bindings } = this.buildBoundFilterClauses(filter)
847
-
848
- try {
849
- return await this.withTransaction(async (tx) => {
850
- const matched = (await tx.query(new BoundQuery(`SELECT id FROM ${table} WHERE ${clause}`, bindings))) as Array<{
851
- id: unknown
852
- }>
853
-
854
- if (matched.length === 0) {
855
- return 0
823
+ deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): AwaitableEffect<number, SurrealDBError> {
824
+ const self = this
825
+ return toAwaitableEffect(
826
+ Effect.gen(function* () {
827
+ assertValidIdentifier(table, 'table name')
828
+ const filterKeys = Object.keys(filter)
829
+ if (filterKeys.length === 0) {
830
+ return yield* new SurrealDBError({ message: `Refusing to delete all records in ${table} without a filter` })
856
831
  }
857
-
858
- for (const row of matched) {
859
- await tx.delete(this.normalizeRecordId(row.id, table))
860
- }
861
-
862
- return matched.length
863
- })
864
- } catch (error) {
865
- return this.toSurrealError(error, `DELETE ${table} WHERE ...`)
866
- }
832
+ const { clause, bindings } = self.buildBoundFilterClauses(filter)
833
+ return yield* self.withTransaction((tx) =>
834
+ Effect.gen(function* () {
835
+ const matchedRows = (yield* tx.query(
836
+ new BoundQuery(`SELECT id FROM ${table} WHERE ${clause}`, bindings),
837
+ )) as Array<{ id: unknown }>
838
+
839
+ if (matchedRows.length === 0) {
840
+ return 0
841
+ }
842
+
843
+ for (const row of matchedRows) {
844
+ yield* tx.delete(self.normalizeRecordId(row.id, table))
845
+ }
846
+
847
+ return matchedRows.length
848
+ }),
849
+ )
850
+ }),
851
+ )
867
852
  }
868
853
 
869
- async updateWhere(table: DatabaseTable, where: ExprLike, data: Record<string, unknown>): Promise<number> {
870
- if (!where) {
871
- throw new SurrealDBError(`Refusing to update records in ${table} without a where clause`)
872
- }
873
- const keys = Object.keys(data)
874
- if (keys.length === 0) {
875
- throw new SurrealDBError(`Cannot update records in ${table} with empty data`)
876
- }
877
-
878
- const client = await this.ensureConnected()
854
+ updateWhere(
855
+ table: DatabaseTable,
856
+ where: ExprLike,
857
+ data: Record<string, unknown>,
858
+ ): AwaitableEffect<number, SurrealDBError> {
859
+ const self = this
860
+ return toAwaitableEffect(
861
+ Effect.gen(function* () {
862
+ if (!where) {
863
+ return yield* new SurrealDBError({ message: `Refusing to update records in ${table} without a where clause` })
864
+ }
865
+ const keys = Object.keys(data)
866
+ if (keys.length === 0) {
867
+ return yield* new SurrealDBError({ message: `Cannot update records in ${table} with empty data` })
868
+ }
879
869
 
880
- try {
881
- const updated = await client
882
- .update<unknown>(new Table(table))
883
- .where(where)
884
- .merge(this.normalizeMutationData(data))
885
- .output('after')
886
- if (Array.isArray(updated)) {
887
- return updated.length
888
- }
889
- return 1
890
- } catch (error) {
891
- return this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
892
- }
870
+ const client = yield* self.ensureConnectedEffect(`UPDATE ${table} WHERE ...`)
871
+ const updated = yield* Effect.tryPromise({
872
+ try: () =>
873
+ client
874
+ .update<unknown>(new Table(table))
875
+ .where(where)
876
+ .merge(self.normalizeMutationData(data))
877
+ .output('after'),
878
+ catch: (error) =>
879
+ new SurrealDBError({
880
+ message: `Failed to update records in ${table}: ${getErrorMessage(error)}`,
881
+ cause: error,
882
+ }),
883
+ })
884
+ return Array.isArray(updated) ? updated.length : 1
885
+ }),
886
+ )
893
887
  }
894
888
 
895
- async insert<T extends Record<string, unknown>>(
889
+ insert<T extends Record<string, unknown>>(
896
890
  table: DatabaseTable,
897
891
  data: Values<T> | Array<Values<T>>,
898
- ): Promise<T[]> {
899
- const client = await this.ensureConnected()
900
-
901
- try {
902
- const normalized = Array.isArray(data)
903
- ? data.map((item) => this.normalizeMutationData(item as Record<string, unknown>))
904
- : this.normalizeMutationData(data as Record<string, unknown>)
905
- const inserted = await client
906
- .insert<T>(new Table(table), normalized as Values<T> | Array<Values<T>>)
907
- .output('after')
908
- return inserted as T[]
909
- } catch (error) {
910
- return this.toSurrealError(error, `INSERT ${table}`)
911
- }
892
+ ): AwaitableEffect<T[], SurrealDBError> {
893
+ const self = this
894
+ return toAwaitableEffect(
895
+ Effect.gen(function* () {
896
+ const client = yield* self.ensureConnectedEffect(`INSERT ${table}`)
897
+ const normalized = Array.isArray(data)
898
+ ? data.map((item) => self.normalizeMutationData(item as Record<string, unknown>))
899
+ : self.normalizeMutationData(data as Record<string, unknown>)
900
+ const inserted = yield* Effect.tryPromise({
901
+ try: () => client.insert<T>(new Table(table), normalized as Values<T> | Array<Values<T>>).output('after'),
902
+ catch: (error) =>
903
+ new SurrealDBError({
904
+ message: `Failed to insert rows into ${table}: ${getErrorMessage(error)}`,
905
+ cause: error,
906
+ }),
907
+ })
908
+ return inserted as T[]
909
+ }),
910
+ )
912
911
  }
913
912
 
914
- async relate<T extends Record<string, unknown>>(
913
+ relate<T extends Record<string, unknown>>(
915
914
  from: RecordIdInput,
916
915
  edgeTable: DatabaseTable,
917
916
  to: RecordIdInput,
918
917
  data?: Values<T>,
919
- ): Promise<T | null> {
920
- const client = await this.ensureConnected()
918
+ ): AwaitableEffect<T | null, SurrealDBError> {
921
919
  const fromRef = ensureRecordId(from)
922
920
  const toRef = ensureRecordId(to)
923
921
 
924
- try {
925
- const normalizedData = data
926
- ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<T>)
927
- : undefined
928
- const related = (await client.relate<T>(fromRef, new Table(edgeTable), toRef, normalizedData).output('after')) as
929
- | T
930
- | T[]
931
- | null
932
- if (related === null) {
933
- return null
934
- }
935
- if (Array.isArray(related)) {
936
- return related.at(0) ?? null
937
- }
938
- return related
939
- } catch (error) {
940
- return this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
941
- }
942
- }
943
-
944
- async beginTransaction(): Promise<DatabaseTransaction> {
945
- const client = await this.ensureConnected()
946
- try {
947
- return this.wrapTransaction(await client.beginTransaction())
948
- } catch (error) {
949
- return this.toSurrealError(error, 'BEGIN TRANSACTION')
950
- }
951
- }
952
-
953
- async withTransaction<T>(work: (tx: DatabaseTransaction) => Promise<T>): Promise<T> {
954
- const tx = await this.beginTransaction()
955
- try {
956
- const result = await work(tx)
957
- await tx.commit()
958
- return result
959
- } catch (error) {
960
- try {
961
- await tx.cancel()
962
- } catch (cancelError) {
963
- this.logger?.warn?.(
964
- `Failed to cancel transaction after error: ${
965
- cancelError instanceof Error ? cancelError.message : String(cancelError)
966
- }`,
922
+ const self = this
923
+ return toAwaitableEffect(
924
+ Effect.gen(function* () {
925
+ const client = yield* self.ensureConnectedEffect(
926
+ `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`,
967
927
  )
968
- }
969
- throw error
970
- }
928
+ const normalizedData = data
929
+ ? (self.normalizeMutationData(data as Record<string, unknown>) as Values<T>)
930
+ : undefined
931
+ const related = (yield* Effect.tryPromise({
932
+ try: () => client.relate<T>(fromRef, new Table(edgeTable), toRef, normalizedData).output('after'),
933
+ catch: (error) =>
934
+ new SurrealDBError({
935
+ message: `Failed to relate records ${fromRef.toString()} -> ${edgeTable} -> ${toRef.toString()}: ${getErrorMessage(error)}`,
936
+ cause: error,
937
+ }),
938
+ })) as T | T[] | null
939
+ if (related === null) {
940
+ return null
941
+ }
942
+ if (Array.isArray(related)) {
943
+ return related.at(0) ?? null
944
+ }
945
+ return related
946
+ }),
947
+ )
971
948
  }
972
- }
973
949
 
974
- let currentDatabaseService: SurrealDBService | undefined
975
- const databaseServiceOverrides = new Map<PropertyKey, unknown>()
976
-
977
- function bindTargetMethod(
978
- target: Record<PropertyKey, unknown>,
979
- value: (...args: unknown[]) => unknown,
980
- ): (...args: unknown[]) => unknown {
981
- return (...args) => Reflect.apply(value, target, args)
982
- }
983
-
984
- function resolveConfiguredDatabaseService(): SurrealDBService {
985
- if (!currentDatabaseService) {
986
- throw new SurrealDBError('Database service not configured')
950
+ beginTransaction(): AwaitableEffect<DatabaseTransaction, SurrealDBError> {
951
+ const self = this
952
+ return toAwaitableEffect(
953
+ Effect.gen(function* () {
954
+ const client = yield* self.ensureConnectedEffect('BEGIN TRANSACTION')
955
+ const tx = yield* Effect.tryPromise({
956
+ try: () => client.beginTransaction(),
957
+ catch: (error) =>
958
+ new SurrealDBError({ message: `Failed to begin transaction: ${getErrorMessage(error)}`, cause: error }),
959
+ })
960
+ return self.wrapTransaction(tx)
961
+ }),
962
+ )
987
963
  }
988
964
 
989
- return currentDatabaseService
990
- }
991
-
992
- export const databaseService = new Proxy({} as SurrealDBService, {
993
- get(_target, property) {
994
- if (databaseServiceOverrides.has(property)) {
995
- return databaseServiceOverrides.get(property)
996
- }
997
-
998
- const resolved = resolveConfiguredDatabaseService()
999
- const value: unknown = Reflect.get(resolved, property)
1000
- if (typeof value === 'function') {
1001
- // SurrealDB SDK type gap — Reflect.get returns unknown, but we know the resolved target shape
1002
- return bindTargetMethod(
1003
- resolved as unknown as Record<PropertyKey, unknown>,
1004
- value as (...args: unknown[]) => unknown,
965
+ withTransaction<T, E, R>(
966
+ work: (tx: DatabaseTransaction) => Effect.Effect<T, E, R>,
967
+ ): Effect.Effect<T, SurrealDBError | E, R> {
968
+ const self = this
969
+ return Effect.gen(function* () {
970
+ const tx = yield* self.beginTransaction()
971
+ const cancelEffect = tx.cancel().pipe(
972
+ Effect.catch((cancelError: SurrealDBError) =>
973
+ Effect.sync(() => {
974
+ self.logger?.warn?.(`Failed to cancel transaction after error: ${cancelError.message}`)
975
+ }),
976
+ ),
977
+ Effect.asVoid,
1005
978
  )
1006
- }
1007
979
 
1008
- return value
1009
- },
1010
- set(_target, property, value) {
1011
- databaseServiceOverrides.set(property, value)
1012
- return true
1013
- },
1014
- })
1015
-
1016
- export function setDatabaseService(db: SurrealDBService): void {
1017
- if (db === databaseService) {
1018
- return
980
+ return yield* Effect.gen(function* () {
981
+ const result = yield* work(tx)
982
+ yield* tx.commit()
983
+ return result
984
+ }).pipe(Effect.onError(() => cancelEffect))
985
+ })
1019
986
  }
1020
-
1021
- currentDatabaseService = db
1022
987
  }