@lota-sdk/core 0.4.9 → 0.4.11

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