@lota-sdk/core 0.4.8 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -12
- package/src/ai/embedding-cache.ts +96 -22
- package/src/ai-gateway/ai-gateway.ts +766 -223
- package/src/config/agent-defaults.ts +189 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/background-processing.ts +1 -1
- package/src/config/constants.ts +8 -2
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +299 -19
- package/src/config/thread-defaults.ts +40 -20
- package/src/create-runtime.ts +200 -449
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +868 -601
- package/src/db/memory.ts +396 -280
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +288 -0
- package/src/db/service.ts +912 -779
- package/src/db/startup.ts +153 -68
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +96 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +123 -0
- package/src/effect/index.ts +24 -0
- package/src/effect/layers.ts +238 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +46 -0
- package/src/effect/services.ts +61 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +128 -83
- package/src/index.ts +48 -1
- package/src/openrouter/direct-provider.ts +11 -35
- package/src/queues/autonomous-job.queue.ts +117 -73
- package/src/queues/context-compaction.queue.ts +50 -17
- package/src/queues/delayed-node-promotion.queue.ts +46 -17
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +26 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
- package/src/queues/plan-scheduler.queue.ts +97 -33
- package/src/queues/post-chat-memory.queue.ts +56 -26
- package/src/queues/queue-factory.ts +227 -59
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +45 -11
- package/src/redis/connection.ts +182 -113
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +20 -0
- package/src/redis/stream-context.ts +92 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +5 -2
- package/src/runtime/agent-stream-helpers.ts +24 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +16 -4
- package/src/runtime/helper-model.ts +139 -48
- package/src/runtime/index.ts +7 -8
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/memory/memory-scope.ts +53 -0
- package/src/runtime/plugin-resolution.ts +124 -25
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +177 -130
- package/src/runtime/retrieval-adapters.ts +40 -6
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +150 -61
- package/src/runtime/runtime-extensions.ts +23 -25
- package/src/runtime/runtime-lifecycle.ts +124 -0
- package/src/runtime/runtime-services.ts +386 -0
- package/src/runtime/runtime-token.ts +47 -0
- package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
- package/src/runtime/social-chat/social-chat.ts +630 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +183 -111
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +253 -149
- package/src/services/artifact.service.ts +231 -149
- package/src/services/attachment.service.ts +171 -115
- package/src/services/autonomous-job.service.ts +890 -491
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +13 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +151 -88
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +278 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +101 -168
- package/src/services/graph-full-routing.ts +193 -0
- package/src/services/index.ts +19 -21
- package/src/services/institutional-memory.service.ts +213 -125
- package/src/services/learned-skill.service.ts +368 -260
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +50 -0
- package/src/services/memory/memory-preseeded.ts +86 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
- package/src/services/memory/memory.service.ts +674 -0
- package/src/services/memory/rerank.service.ts +201 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +29 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +153 -77
- package/src/services/ownership-dispatcher.service.ts +456 -263
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +169 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +405 -0
- package/src/services/plan/plan-deadline.service.ts +533 -0
- package/src/services/plan/plan-event-delivery.service.ts +266 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +522 -0
- package/src/services/plan/plan-executor-helpers.ts +307 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1737 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +637 -0
- package/src/services/plan/plan-scheduler.service.ts +379 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +36 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +131 -0
- package/src/services/plugin-executor.service.ts +102 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +288 -231
- package/src/services/recent-activity-title.service.ts +73 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +190 -122
- package/src/services/system-executor.service.ts +96 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +385 -0
- package/src/services/thread/thread-listing.ts +199 -0
- package/src/services/thread/thread-memory-block.ts +130 -0
- package/src/services/thread/thread-message.service.ts +379 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
- package/src/services/thread/thread-turn-streaming.ts +403 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +376 -0
- package/src/services/thread/thread.service.ts +344 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +334 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +3 -3
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +3 -3
- package/src/system-agents/memory.agent.ts +3 -3
- package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
- package/src/system-agents/skill-extractor.agent.ts +3 -3
- package/src/system-agents/skill-manager.agent.ts +3 -3
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +3 -3
- package/src/tools/execution-plan.tool.ts +241 -171
- package/src/tools/fetch-webpage.tool.ts +29 -18
- package/src/tools/firecrawl-client.ts +26 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +57 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +33 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +125 -84
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +25 -22
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +111 -20
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +164 -52
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +74 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/config/search.ts +0 -3
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/agent-types.ts +0 -1
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/memory-scope.ts +0 -43
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -914
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/rerank.service.ts +0 -156
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ManagedRuntime } from 'effect'
|
|
2
|
+
import { Schema, Effect } from 'effect'
|
|
3
|
+
|
|
4
|
+
import { serverLogger } from '../config/logger'
|
|
5
|
+
import { initializeSandboxedWorkerRuntime } from '../workers/bootstrap'
|
|
6
|
+
|
|
7
|
+
class StandaloneQueueWorkerError extends Schema.TaggedErrorClass<StandaloneQueueWorkerError>()(
|
|
8
|
+
'StandaloneQueueWorkerError',
|
|
9
|
+
{ message: Schema.String, cause: Schema.Defect },
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
function toStandaloneQueueWorkerError(cause: unknown): StandaloneQueueWorkerError {
|
|
13
|
+
return new StandaloneQueueWorkerError({ message: cause instanceof Error ? cause.message : String(cause), cause })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line typescript-eslint/no-explicit-any -- wildcard for host-provided ManagedRuntime
|
|
17
|
+
export function runStandaloneQueueWorker(start: (runtime: ManagedRuntime.ManagedRuntime<any, any>) => void): void {
|
|
18
|
+
if (!import.meta.main) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
void Effect.runFork(
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const runtime = yield* Effect.tryPromise({
|
|
25
|
+
try: () => initializeSandboxedWorkerRuntime(),
|
|
26
|
+
catch: (cause) => toStandaloneQueueWorkerError(cause),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
yield* Effect.sync(() => start(runtime))
|
|
30
|
+
}).pipe(
|
|
31
|
+
Effect.catchTag('StandaloneQueueWorkerError', (error) =>
|
|
32
|
+
Effect.sync(() => {
|
|
33
|
+
serverLogger.error`Standalone queue worker failed: ${error.message}`
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Job } from 'bullmq'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import type { Context } from 'effect'
|
|
4
|
+
import type IORedis from 'ioredis'
|
|
2
5
|
|
|
3
6
|
import { ensureRecordId } from '../db/record-id'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
|
|
8
|
+
import { RecentActivityTitleServiceTag } from '../services/recent-activity-title.service'
|
|
9
|
+
import { ThreadTitleServiceTag } from '../services/thread/thread-title.service'
|
|
10
|
+
import { createQueueFactoryWithDeps } from './queue-factory'
|
|
11
|
+
import { runStandaloneQueueWorker } from './standalone-worker'
|
|
8
12
|
|
|
9
13
|
export const TITLE_GENERATION_QUEUE = 'title-generation'
|
|
10
14
|
|
|
@@ -25,23 +29,31 @@ interface RecentActivityTitleRefinementJob {
|
|
|
25
29
|
|
|
26
30
|
type TitleGenerationJob = ThreadTitleGenerationJob | RecentActivityTitleRefinementJob
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
interface TitleGenerationQueueDeps {
|
|
33
|
+
databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
|
|
34
|
+
threadTitleService: Context.Service.Shape<typeof ThreadTitleServiceTag>
|
|
35
|
+
recentActivityTitleService: Context.Service.Shape<typeof RecentActivityTitleServiceTag>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function processTitleGenerationJob(deps: TitleGenerationQueueDeps, job: Job<TitleGenerationJob>): Promise<void> {
|
|
39
|
+
const { threadTitleService, recentActivityTitleService } = deps
|
|
30
40
|
if (job.data.kind === 'thread-title') {
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
return Effect.runPromise(
|
|
42
|
+
Effect.asVoid(threadTitleService.generateAndPersistTitle(ensureRecordId(job.data.threadId), job.data.sourceText)),
|
|
43
|
+
)
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
return Effect.runPromise(Effect.asVoid(recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)))
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
const titleGeneration =
|
|
49
|
+
const titleGeneration = createQueueFactoryWithDeps<TitleGenerationJob, TitleGenerationQueueDeps>({
|
|
39
50
|
name: TITLE_GENERATION_QUEUE,
|
|
40
51
|
displayName: 'Title generation',
|
|
41
52
|
jobName: 'title-generation',
|
|
42
53
|
concurrency: 10,
|
|
43
54
|
lockDuration: 300_000,
|
|
44
55
|
defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
|
|
56
|
+
prepare: ({ databaseService }) => databaseService.connect(),
|
|
45
57
|
processor: processTitleGenerationJob,
|
|
46
58
|
})
|
|
47
59
|
|
|
@@ -56,4 +68,26 @@ export function enqueueRecentActivityTitleRefinement(job: Omit<RecentActivityTit
|
|
|
56
68
|
)
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
export
|
|
71
|
+
export function startTitleGenerationWorker(options: {
|
|
72
|
+
registerSignals?: boolean
|
|
73
|
+
connectionProvider: () => IORedis
|
|
74
|
+
deps: TitleGenerationQueueDeps
|
|
75
|
+
}): ReturnType<typeof titleGeneration.startWorker> {
|
|
76
|
+
return titleGeneration.startWorker({
|
|
77
|
+
deps: options.deps,
|
|
78
|
+
registerSignals: options.registerSignals,
|
|
79
|
+
connectionProvider: options.connectionProvider,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
runStandaloneQueueWorker((runtime) => {
|
|
84
|
+
const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
|
|
85
|
+
startTitleGenerationWorker({
|
|
86
|
+
connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
|
|
87
|
+
deps: {
|
|
88
|
+
databaseService: resolve(DatabaseServiceTag),
|
|
89
|
+
threadTitleService: resolve(ThreadTitleServiceTag),
|
|
90
|
+
recentActivityTitleService: resolve(RecentActivityTitleServiceTag),
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
})
|
package/src/redis/connection.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Duration, Effect, Exit, Scope } from 'effect'
|
|
1
2
|
import IORedis from 'ioredis'
|
|
2
3
|
import type { RedisOptions } from 'ioredis'
|
|
3
4
|
|
|
5
|
+
import { RedisError } from '../effect/errors'
|
|
6
|
+
import { effectTryServicePromise } from '../effect/helpers'
|
|
4
7
|
import { getErrorMessage } from '../utils/errors'
|
|
5
8
|
|
|
6
9
|
export interface RedisConnectionLogger {
|
|
@@ -10,7 +13,7 @@ export interface RedisConnectionLogger {
|
|
|
10
13
|
error?: (message: string) => void
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
interface CreateRedisConnectionManagerOptions {
|
|
16
|
+
export interface CreateRedisConnectionManagerOptions {
|
|
14
17
|
url: string
|
|
15
18
|
redisOptions?: RedisOptions
|
|
16
19
|
healthCheckIntervalMs?: number
|
|
@@ -52,134 +55,200 @@ function log(logger: RedisConnectionLogger | undefined, level: keyof RedisConnec
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
private isInitialized = false
|
|
60
|
-
private isClosing = false
|
|
61
|
-
private isHealthCheckRunning = false
|
|
62
|
-
|
|
63
|
-
constructor(private readonly options: CreateRedisConnectionManagerOptions) {
|
|
64
|
-
this.initializeConnection()
|
|
65
|
-
}
|
|
58
|
+
interface ManagerState {
|
|
59
|
+
isHealthy: boolean
|
|
60
|
+
isClosing: boolean
|
|
61
|
+
}
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Build the IORedis client + listeners as an `Effect.acquireRelease`. Each
|
|
65
|
+
* handler is held as a const ref so the release step can pass the same
|
|
66
|
+
* function to `client.off(...)`, then `quit()` (falling back to
|
|
67
|
+
* `disconnect()`) the client. Prevents tracked-handler leaks on disposal.
|
|
68
|
+
*/
|
|
69
|
+
function acquireRedisClient(
|
|
70
|
+
options: CreateRedisConnectionManagerOptions,
|
|
71
|
+
state: ManagerState,
|
|
72
|
+
): Effect.Effect<IORedis, RedisError, Scope.Scope> {
|
|
73
|
+
return Effect.acquireRelease(
|
|
74
|
+
Effect.try({
|
|
75
|
+
try: () => {
|
|
76
|
+
const redisOptions = options.redisOptions ?? DEFAULT_REDIS_OPTIONS
|
|
77
|
+
const client = new IORedis(options.url, redisOptions)
|
|
78
|
+
|
|
79
|
+
const onConnect = () => {
|
|
80
|
+
log(options.logger, 'info', 'Redis connected')
|
|
81
|
+
state.isHealthy = true
|
|
82
|
+
}
|
|
83
|
+
const onReady = () => {
|
|
84
|
+
log(options.logger, 'info', 'Redis ready')
|
|
85
|
+
state.isHealthy = true
|
|
86
|
+
}
|
|
87
|
+
const onError = (error: Error) => {
|
|
88
|
+
log(options.logger, 'error', `Redis error: ${error.message}`)
|
|
89
|
+
state.isHealthy = false
|
|
90
|
+
}
|
|
91
|
+
const onClose = () => {
|
|
92
|
+
log(
|
|
93
|
+
options.logger,
|
|
94
|
+
state.isClosing ? 'info' : 'warn',
|
|
95
|
+
state.isClosing ? 'Redis connection closed during shutdown' : 'Redis connection closed',
|
|
96
|
+
)
|
|
97
|
+
state.isHealthy = false
|
|
98
|
+
}
|
|
99
|
+
const onReconnecting = () => {
|
|
100
|
+
log(options.logger, 'info', 'Redis reconnecting...')
|
|
101
|
+
}
|
|
102
|
+
const onEnd = () => {
|
|
103
|
+
log(
|
|
104
|
+
options.logger,
|
|
105
|
+
state.isClosing ? 'info' : 'warn',
|
|
106
|
+
state.isClosing ? 'Redis connection ended during shutdown' : 'Redis connection ended',
|
|
107
|
+
)
|
|
108
|
+
state.isHealthy = false
|
|
109
|
+
}
|
|
71
110
|
|
|
72
|
-
|
|
111
|
+
client.on('connect', onConnect)
|
|
112
|
+
client.on('ready', onReady)
|
|
113
|
+
client.on('error', onError)
|
|
114
|
+
client.on('close', onClose)
|
|
115
|
+
client.on('reconnecting', onReconnecting)
|
|
116
|
+
client.on('end', onEnd)
|
|
117
|
+
|
|
118
|
+
// Carry the handler refs alongside the client so the release closure
|
|
119
|
+
// can detach exactly what was attached.
|
|
120
|
+
;(client as ClientWithHandlers).__lotaHandlers = {
|
|
121
|
+
connect: onConnect,
|
|
122
|
+
ready: onReady,
|
|
123
|
+
error: onError,
|
|
124
|
+
close: onClose,
|
|
125
|
+
reconnecting: onReconnecting,
|
|
126
|
+
end: onEnd,
|
|
127
|
+
}
|
|
73
128
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
129
|
+
return client
|
|
130
|
+
},
|
|
131
|
+
catch: (cause) =>
|
|
132
|
+
new RedisError({ message: `Failed to initialize Redis connection: ${getErrorMessage(cause)}`, cause }),
|
|
133
|
+
}),
|
|
134
|
+
(client) =>
|
|
135
|
+
Effect.gen(function* () {
|
|
136
|
+
state.isClosing = true
|
|
137
|
+
|
|
138
|
+
const handlers = (client as ClientWithHandlers).__lotaHandlers
|
|
139
|
+
if (handlers) {
|
|
140
|
+
client.off('connect', handlers.connect)
|
|
141
|
+
client.off('ready', handlers.ready)
|
|
142
|
+
client.off('error', handlers.error)
|
|
143
|
+
client.off('close', handlers.close)
|
|
144
|
+
client.off('reconnecting', handlers.reconnecting)
|
|
145
|
+
client.off('end', handlers.end)
|
|
146
|
+
}
|
|
83
147
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this.redis.on('error', (error: Error) => {
|
|
98
|
-
log(this.options.logger, 'error', `Redis error: ${error.message}`)
|
|
99
|
-
this.isHealthy = false
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
this.redis.on('close', () => {
|
|
103
|
-
log(
|
|
104
|
-
this.options.logger,
|
|
105
|
-
this.isClosing ? 'info' : 'warn',
|
|
106
|
-
this.isClosing ? 'Redis connection closed during shutdown' : 'Redis connection closed',
|
|
107
|
-
)
|
|
108
|
-
this.isHealthy = false
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
this.redis.on('reconnecting', () => {
|
|
112
|
-
log(this.options.logger, 'info', 'Redis reconnecting...')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
this.redis.on('end', () => {
|
|
116
|
-
log(
|
|
117
|
-
this.options.logger,
|
|
118
|
-
this.isClosing ? 'info' : 'warn',
|
|
119
|
-
this.isClosing ? 'Redis connection ended during shutdown' : 'Redis connection ended',
|
|
120
|
-
)
|
|
121
|
-
this.isHealthy = false
|
|
122
|
-
})
|
|
123
|
-
}
|
|
148
|
+
if (client.status !== 'end') {
|
|
149
|
+
const quitExit = yield* Effect.exit(
|
|
150
|
+
effectTryServicePromise(() => client.quit(), 'Failed to close Redis connection manager'),
|
|
151
|
+
)
|
|
152
|
+
if (Exit.isFailure(quitExit)) {
|
|
153
|
+
log(options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(quitExit.cause)}`)
|
|
154
|
+
client.disconnect()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
124
160
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
interface RedisHandlers {
|
|
162
|
+
readonly connect: () => void
|
|
163
|
+
readonly ready: () => void
|
|
164
|
+
readonly error: (error: Error) => void
|
|
165
|
+
readonly close: () => void
|
|
166
|
+
readonly reconnecting: () => void
|
|
167
|
+
readonly end: () => void
|
|
168
|
+
}
|
|
129
169
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
170
|
+
type ClientWithHandlers = IORedis & { __lotaHandlers?: RedisHandlers }
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Health-check loop wired via `Effect.forkScoped` so interruption is automatic
|
|
174
|
+
* when the owning scope closes. No more imperative fiber tracking.
|
|
175
|
+
*/
|
|
176
|
+
function startHealthCheckFiber(
|
|
177
|
+
client: IORedis,
|
|
178
|
+
options: CreateRedisConnectionManagerOptions,
|
|
179
|
+
state: ManagerState,
|
|
180
|
+
): Effect.Effect<void, never, Scope.Scope> {
|
|
181
|
+
const intervalMs = options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
|
|
182
|
+
const logger = options.logger
|
|
183
|
+
let isHealthCheckRunning = false
|
|
184
|
+
|
|
185
|
+
const loop = Effect.gen(function* () {
|
|
186
|
+
for (;;) {
|
|
187
|
+
yield* Effect.sleep(Duration.millis(intervalMs))
|
|
188
|
+
if (isHealthCheckRunning) continue
|
|
189
|
+
|
|
190
|
+
isHealthCheckRunning = true
|
|
134
191
|
try {
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
192
|
+
if (client.status === 'ready') {
|
|
193
|
+
const pingExit = yield* Effect.exit(effectTryServicePromise(() => client.ping(), 'Redis health check failed'))
|
|
194
|
+
if (Exit.isFailure(pingExit)) {
|
|
195
|
+
log(logger, 'warn', `Redis health check failed: ${getErrorMessage(pingExit.cause)}`)
|
|
196
|
+
state.isHealthy = false
|
|
197
|
+
} else {
|
|
198
|
+
state.isHealthy = true
|
|
199
|
+
}
|
|
138
200
|
}
|
|
139
|
-
} catch (error) {
|
|
140
|
-
log(this.options.logger, 'warn', `Redis health check failed: ${getErrorMessage(error)}`)
|
|
141
|
-
this.isHealthy = false
|
|
142
201
|
} finally {
|
|
143
|
-
|
|
202
|
+
isHealthCheckRunning = false
|
|
144
203
|
}
|
|
145
|
-
}, intervalMs)
|
|
146
|
-
this.healthCheckInterval.unref()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
getConnection(): IORedis {
|
|
150
|
-
if (!this.redis) {
|
|
151
|
-
throw new Error('Redis connection not initialized')
|
|
152
204
|
}
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
getConnectionForBullMQ(): IORedis {
|
|
157
|
-
return this.getConnection()
|
|
158
|
-
}
|
|
205
|
+
})
|
|
159
206
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
207
|
+
return Effect.asVoid(Effect.forkScoped(loop))
|
|
208
|
+
}
|
|
163
209
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Scoped builder for the Redis connection manager. The IORedis client and
|
|
212
|
+
* health-check fiber are both bound to the surrounding scope, so closing the
|
|
213
|
+
* scope tears down listeners + quits the client + interrupts the fiber.
|
|
214
|
+
*/
|
|
215
|
+
export function makeRedisConnectionManager(
|
|
216
|
+
options: CreateRedisConnectionManagerOptions,
|
|
217
|
+
): Effect.Effect<RedisConnectionManager, RedisError, Scope.Scope> {
|
|
218
|
+
return Effect.gen(function* () {
|
|
219
|
+
const state: ManagerState = { isHealthy: false, isClosing: false }
|
|
220
|
+
const client = yield* acquireRedisClient(options, state)
|
|
221
|
+
yield* startHealthCheckFiber(client, options, state)
|
|
222
|
+
|
|
223
|
+
const manager: RedisConnectionManager = {
|
|
224
|
+
getConnection: () => client,
|
|
225
|
+
getConnectionForBullMQ: () => client,
|
|
226
|
+
isConnectionHealthy: () => state.isHealthy && client.status === 'ready',
|
|
227
|
+
closeConnection: () => Promise.resolve(),
|
|
168
228
|
}
|
|
169
229
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
if (this.redis && this.redis.status !== 'end') {
|
|
174
|
-
await this.redis.quit()
|
|
175
|
-
}
|
|
176
|
-
} catch (error) {
|
|
177
|
-
log(this.options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(error)}`)
|
|
178
|
-
this.redis?.disconnect()
|
|
179
|
-
}
|
|
180
|
-
}
|
|
230
|
+
return manager
|
|
231
|
+
})
|
|
181
232
|
}
|
|
182
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Standalone Promise-friendly factory for hosts that build the manager outside
|
|
236
|
+
* a Layer. Owns its own `Scope`; `closeConnection()` closes that scope, which
|
|
237
|
+
* runs the same finalizer chain (listener removal, quit/disconnect, fiber
|
|
238
|
+
* interrupt) that `Layer.effect` runs at runtime shutdown.
|
|
239
|
+
*/
|
|
183
240
|
export function createRedisConnectionManager(options: CreateRedisConnectionManagerOptions): RedisConnectionManager {
|
|
184
|
-
|
|
241
|
+
const scope = Scope.makeUnsafe()
|
|
242
|
+
const built = Effect.runSync(Scope.provide(makeRedisConnectionManager(options), scope))
|
|
243
|
+
let closePromise: Promise<void> | null = null
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...built,
|
|
247
|
+
closeConnection: () => {
|
|
248
|
+
if (!closePromise) {
|
|
249
|
+
closePromise = Effect.runPromise(Scope.close(scope, Exit.void).pipe(Effect.catchCause(() => Effect.void)))
|
|
250
|
+
}
|
|
251
|
+
return closePromise
|
|
252
|
+
},
|
|
253
|
+
}
|
|
185
254
|
}
|
package/src/redis/index.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { createRedisConnectionManager } from './connection'
|
|
2
2
|
import type { RedisConnectionManager } from './connection'
|
|
3
3
|
export { DEFAULT_REDIS_OPTIONS, type RedisConnectionLogger } from './connection'
|
|
4
|
+
export { withOrgMemoryLock, withOrgMemoryLockEffect } from './org-memory-lock'
|
|
5
|
+
export { withLeaseLock } from './redis-lease-lock'
|
|
4
6
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './connection-accessor'
|
|
10
|
-
export { withOrgMemoryLock } from './org-memory-lock'
|
|
11
|
-
export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
|
|
12
|
-
export { closeSharedSubscriber, createThreadResumableContext } from './stream-context'
|
|
7
|
+
createThreadResumableContext,
|
|
8
|
+
SharedThreadStreamSubscriberLive,
|
|
9
|
+
SharedThreadStreamSubscriberTag,
|
|
10
|
+
} from './stream-context'
|
|
13
11
|
|
|
14
12
|
export { createRedisConnectionManager }
|
|
15
13
|
export type { RedisConnectionManager }
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { Schema, Effect } from 'effect'
|
|
2
|
+
import type IORedis from 'ioredis'
|
|
3
|
+
|
|
1
4
|
import { serverLogger } from '../config/logger'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
5
|
+
import { LockAcquisitionError } from '../effect/errors'
|
|
6
|
+
import type { LockLostError, RedisError } from '../effect/errors'
|
|
7
|
+
import { withLeaseLock } from './redis-lease-lock'
|
|
8
|
+
import type { RedisLeaseLockOptions } from './redis-lease-lock'
|
|
9
|
+
import { getRuntimeRedisConnection } from './runtime-connection'
|
|
4
10
|
|
|
5
11
|
const ORG_MEMORY_LOCK_PREFIX = 'lock:org-memory:org:'
|
|
6
12
|
const ORG_MEMORY_LOCK_TTL_MS = 120_000
|
|
@@ -9,35 +15,62 @@ const ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS = 30_000
|
|
|
9
15
|
const ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS = 30_000
|
|
10
16
|
const ORG_MEMORY_LOCK_MAX_WAIT_MS = 15 * 60 * 1000
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
class OrgMemoryLockCallbackError extends Schema.TaggedErrorClass<OrgMemoryLockCallbackError>()(
|
|
19
|
+
'OrgMemoryLockCallbackError',
|
|
20
|
+
{ message: Schema.String, cause: Schema.Defect },
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
function createOrgMemoryLockOptions(redis: IORedis, orgId: string): RedisLeaseLockOptions & { redis: IORedis } {
|
|
24
|
+
return {
|
|
25
|
+
redis,
|
|
26
|
+
lockKey: `${ORG_MEMORY_LOCK_PREFIX}${orgId}`,
|
|
27
|
+
lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
|
|
28
|
+
retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
|
|
29
|
+
refreshIntervalMs: ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS,
|
|
30
|
+
waitLogIntervalMs: ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS,
|
|
31
|
+
maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
|
|
32
|
+
label: 'org memory lock',
|
|
33
|
+
logger: {
|
|
34
|
+
debug: (message) => {
|
|
35
|
+
serverLogger.debug`${message}`
|
|
36
|
+
},
|
|
37
|
+
info: (message) => {
|
|
38
|
+
serverLogger.info`${message}`
|
|
39
|
+
},
|
|
40
|
+
warn: (message) => {
|
|
41
|
+
serverLogger.warn`${message}`
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function withOrgMemoryLock<T>(orgId: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
48
|
+
return Effect.runPromise(
|
|
49
|
+
withOrgMemoryLockEffect(orgId, (signal) =>
|
|
50
|
+
Effect.tryPromise({
|
|
51
|
+
try: () => fn(signal),
|
|
52
|
+
catch: (cause) => new OrgMemoryLockCallbackError({ message: 'Org memory lock callback failed.', cause }),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function withOrgMemoryLockEffect<A, E>(
|
|
59
|
+
orgId: string,
|
|
60
|
+
fn: (signal: AbortSignal) => Effect.Effect<A, E>,
|
|
61
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError> {
|
|
13
62
|
const normalizedOrgId = orgId.trim()
|
|
14
63
|
|
|
15
64
|
if (!normalizedOrgId) {
|
|
16
|
-
|
|
65
|
+
return Effect.fail(
|
|
66
|
+
new LockAcquisitionError({
|
|
67
|
+
lockKey: `${ORG_MEMORY_LOCK_PREFIX}<missing-org-id>`,
|
|
68
|
+
maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
17
71
|
}
|
|
18
72
|
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
redis: getRedisConnection(),
|
|
22
|
-
lockKey: `${ORG_MEMORY_LOCK_PREFIX}${normalizedOrgId}`,
|
|
23
|
-
lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
|
|
24
|
-
retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
|
|
25
|
-
refreshIntervalMs: ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS,
|
|
26
|
-
waitLogIntervalMs: ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS,
|
|
27
|
-
maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
|
|
28
|
-
label: 'org memory lock',
|
|
29
|
-
logger: {
|
|
30
|
-
debug: (message) => {
|
|
31
|
-
serverLogger.debug`${message}`
|
|
32
|
-
},
|
|
33
|
-
info: (message) => {
|
|
34
|
-
serverLogger.info`${message}`
|
|
35
|
-
},
|
|
36
|
-
warn: (message) => {
|
|
37
|
-
serverLogger.warn`${message}`
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
fn,
|
|
73
|
+
return Effect.sync(() => getRuntimeRedisConnection()).pipe(
|
|
74
|
+
Effect.flatMap((redis) => withLeaseLock(createOrgMemoryLockOptions(redis, normalizedOrgId), fn)),
|
|
42
75
|
)
|
|
43
76
|
}
|