|
|
@@ -1,5 +1,5 @@
|
|
|
import * as InstanceState from "@/effect/instance-state"
|
|
|
-import { AppRuntime } from "@/effect/app-runtime"
|
|
|
+import { EffectBridge } from "@/effect/bridge"
|
|
|
import { Agent } from "@/agent/agent"
|
|
|
import { Bus } from "@/bus"
|
|
|
import { Command } from "@/command"
|
|
|
@@ -53,6 +53,13 @@ const mapNotFound = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
|
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
|
|
|
Effect.gen(function* () {
|
|
|
const session = yield* Session.Service
|
|
|
+ const shareSvc = yield* SessionShare.Service
|
|
|
+ const promptSvc = yield* SessionPrompt.Service
|
|
|
+ const revertSvc = yield* SessionRevert.Service
|
|
|
+ const compactSvc = yield* SessionCompaction.Service
|
|
|
+ const runState = yield* SessionRunState.Service
|
|
|
+ const agentSvc = yield* Agent.Service
|
|
|
+ const permissionSvc = yield* Permission.Service
|
|
|
const statusSvc = yield* SessionStatus.Service
|
|
|
const todoSvc = yield* Todo.Service
|
|
|
const summary = yield* SessionSummary.Service
|
|
|
@@ -148,14 +155,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
})
|
|
|
|
|
|
const create = Effect.fn("SessionHttpApi.create")(function* (ctx: { payload?: Session.CreateInput }) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionShare.Service.use((svc) => svc.create(ctx.payload)).pipe(Effect.provide(SessionShare.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* shareSvc.create(ctx.payload)
|
|
|
})
|
|
|
|
|
|
const createRaw = Effect.fn("SessionHttpApi.createRaw")(function* (ctx: {
|
|
|
@@ -175,14 +175,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
})
|
|
|
|
|
|
const remove = Effect.fn("SessionHttpApi.remove")(function* (ctx: { params: { sessionID: SessionID } }) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Session.Service.use((svc) => svc.remove(ctx.params.sessionID)).pipe(Effect.provide(Session.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* session.remove(ctx.params.sessionID)
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
@@ -190,60 +183,31 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof UpdatePayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Session.Service.use((svc) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const current = yield* svc.get(ctx.params.sessionID)
|
|
|
- if (ctx.payload.title !== undefined) {
|
|
|
- yield* svc.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title })
|
|
|
- }
|
|
|
- if (ctx.payload.permission !== undefined) {
|
|
|
- yield* svc.setPermission({
|
|
|
- sessionID: ctx.params.sessionID,
|
|
|
- permission: Permission.merge(current.permission ?? [], ctx.payload.permission),
|
|
|
- })
|
|
|
- }
|
|
|
- if (ctx.payload.time?.archived !== undefined) {
|
|
|
- yield* svc.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived })
|
|
|
- }
|
|
|
- return yield* svc.get(ctx.params.sessionID)
|
|
|
- }),
|
|
|
- ).pipe(Effect.provide(Session.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ const current = yield* session.get(ctx.params.sessionID)
|
|
|
+ if (ctx.payload.title !== undefined) {
|
|
|
+ yield* session.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title })
|
|
|
+ }
|
|
|
+ if (ctx.payload.permission !== undefined) {
|
|
|
+ yield* session.setPermission({
|
|
|
+ sessionID: ctx.params.sessionID,
|
|
|
+ permission: Permission.merge(current.permission ?? [], ctx.payload.permission),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (ctx.payload.time?.archived !== undefined) {
|
|
|
+ yield* session.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived })
|
|
|
+ }
|
|
|
+ return yield* session.get(ctx.params.sessionID)
|
|
|
})
|
|
|
|
|
|
const fork = Effect.fn("SessionHttpApi.fork")(function* (ctx: {
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof ForkPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Session.Service.use((svc) =>
|
|
|
- svc.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }),
|
|
|
- ).pipe(Effect.provide(Session.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID })
|
|
|
})
|
|
|
|
|
|
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionPrompt.Service.use((svc) => svc.cancel(ctx.params.sessionID)).pipe(
|
|
|
- Effect.provide(SessionPrompt.defaultLayer),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* promptSvc.cancel(ctx.params.sessionID)
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
@@ -251,98 +215,45 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof InitPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionPrompt.Service.use((svc) =>
|
|
|
- svc.command({
|
|
|
- sessionID: ctx.params.sessionID,
|
|
|
- messageID: ctx.payload.messageID,
|
|
|
- model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
|
|
- command: Command.Default.INIT,
|
|
|
- arguments: "",
|
|
|
- }),
|
|
|
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* promptSvc.command({
|
|
|
+ sessionID: ctx.params.sessionID,
|
|
|
+ messageID: ctx.payload.messageID,
|
|
|
+ model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
|
|
+ command: Command.Default.INIT,
|
|
|
+ arguments: "",
|
|
|
+ })
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
const share = Effect.fn("SessionHttpApi.share")(function* (ctx: { params: { sessionID: SessionID } }) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Effect.gen(function* () {
|
|
|
- const share = yield* SessionShare.Service
|
|
|
- const session = yield* Session.Service
|
|
|
- yield* share.share(ctx.params.sessionID)
|
|
|
- return yield* session.get(ctx.params.sessionID)
|
|
|
- }).pipe(Effect.provide(SessionShare.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* shareSvc.share(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
|
|
+ return yield* session.get(ctx.params.sessionID)
|
|
|
})
|
|
|
|
|
|
const unshare = Effect.fn("SessionHttpApi.unshare")(function* (ctx: { params: { sessionID: SessionID } }) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Effect.gen(function* () {
|
|
|
- const share = yield* SessionShare.Service
|
|
|
- const session = yield* Session.Service
|
|
|
- yield* share.unshare(ctx.params.sessionID)
|
|
|
- return yield* session.get(ctx.params.sessionID)
|
|
|
- }).pipe(Effect.provide(SessionShare.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* shareSvc.unshare(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
|
|
+ return yield* session.get(ctx.params.sessionID)
|
|
|
})
|
|
|
|
|
|
const summarize = Effect.fn("SessionHttpApi.summarize")(function* (ctx: {
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof SummarizePayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Effect.gen(function* () {
|
|
|
- const session = yield* Session.Service
|
|
|
- const revert = yield* SessionRevert.Service
|
|
|
- const compact = yield* SessionCompaction.Service
|
|
|
- const prompt = yield* SessionPrompt.Service
|
|
|
- const agent = yield* Agent.Service
|
|
|
-
|
|
|
- yield* revert.cleanup(yield* session.get(ctx.params.sessionID))
|
|
|
- const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
|
|
- const defaultAgent = yield* agent.defaultAgent()
|
|
|
- const currentAgent =
|
|
|
- messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
|
|
|
-
|
|
|
- yield* compact.create({
|
|
|
- sessionID: ctx.params.sessionID,
|
|
|
- agent: currentAgent,
|
|
|
- model: {
|
|
|
- providerID: ctx.payload.providerID,
|
|
|
- modelID: ctx.payload.modelID,
|
|
|
- },
|
|
|
- auto: ctx.payload.auto ?? false,
|
|
|
- })
|
|
|
- yield* prompt.loop({ sessionID: ctx.params.sessionID })
|
|
|
- }).pipe(
|
|
|
- Effect.provide(SessionRevert.defaultLayer),
|
|
|
- Effect.provide(SessionCompaction.defaultLayer),
|
|
|
- Effect.provide(SessionPrompt.defaultLayer),
|
|
|
- Effect.provide(Agent.defaultLayer),
|
|
|
- Effect.provide(Session.defaultLayer),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
|
|
|
+ const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
|
|
+ const defaultAgent = yield* agentSvc.defaultAgent()
|
|
|
+ const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
|
|
|
+
|
|
|
+ yield* compactSvc.create({
|
|
|
+ sessionID: ctx.params.sessionID,
|
|
|
+ agent: currentAgent,
|
|
|
+ model: {
|
|
|
+ providerID: ctx.payload.providerID,
|
|
|
+ modelID: ctx.payload.modelID,
|
|
|
+ },
|
|
|
+ auto: ctx.payload.auto ?? false,
|
|
|
+ })
|
|
|
+ yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
@@ -350,19 +261,15 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof PromptPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
+ const bridge = yield* EffectBridge.make()
|
|
|
return HttpServerResponse.stream(
|
|
|
Stream.fromEffect(
|
|
|
Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionPrompt.Service.use((svc) =>
|
|
|
- svc.prompt({
|
|
|
- ...ctx.payload,
|
|
|
- sessionID: ctx.params.sessionID,
|
|
|
- } as unknown as SessionPrompt.PromptInput),
|
|
|
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
|
- ),
|
|
|
+ bridge.promise(
|
|
|
+ promptSvc.prompt({
|
|
|
+ ...ctx.payload,
|
|
|
+ sessionID: ctx.params.sessionID,
|
|
|
+ } as unknown as SessionPrompt.PromptInput),
|
|
|
),
|
|
|
),
|
|
|
).pipe(
|
|
|
@@ -377,23 +284,23 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof PromptPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
+ const bridge = yield* EffectBridge.make()
|
|
|
yield* Effect.sync(() => {
|
|
|
- Instance.restore(instance, () => {
|
|
|
- void AppRuntime.runPromise(
|
|
|
- SessionPrompt.Service.use((svc) =>
|
|
|
- svc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput),
|
|
|
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
|
- ).catch((error) => {
|
|
|
- log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error })
|
|
|
- void Bus.publish(Session.Event.Error, {
|
|
|
- sessionID: ctx.params.sessionID,
|
|
|
- error: new NamedError.Unknown({
|
|
|
- message: error instanceof Error ? error.message : String(error),
|
|
|
- }).toObject(),
|
|
|
- })
|
|
|
- })
|
|
|
- })
|
|
|
+ bridge.fork(
|
|
|
+ promptSvc
|
|
|
+ .prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput)
|
|
|
+ .pipe(
|
|
|
+ Effect.catchCause((error) =>
|
|
|
+ Effect.sync(() => {
|
|
|
+ log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error })
|
|
|
+ void Bus.publish(Session.Event.Error, {
|
|
|
+ sessionID: ctx.params.sessionID,
|
|
|
+ error: new NamedError.Unknown({ message: String(error) }).toObject(),
|
|
|
+ })
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ )
|
|
|
})
|
|
|
return HttpApiSchema.NoContent.make()
|
|
|
})
|
|
|
@@ -402,111 +309,47 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof CommandPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionPrompt.Service.use((svc) =>
|
|
|
- svc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput),
|
|
|
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput)
|
|
|
})
|
|
|
|
|
|
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof ShellPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionPrompt.Service.use((svc) =>
|
|
|
- svc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput),
|
|
|
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput)
|
|
|
})
|
|
|
|
|
|
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
|
|
|
params: { sessionID: SessionID }
|
|
|
payload: typeof RevertPayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- log.info("revert", ctx.payload)
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionRevert.Service.use((svc) => svc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })).pipe(
|
|
|
- Effect.provide(SessionRevert.defaultLayer),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })
|
|
|
})
|
|
|
|
|
|
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- SessionRevert.Service.use((svc) => svc.unrevert({ sessionID: ctx.params.sessionID })).pipe(
|
|
|
- Effect.provide(SessionRevert.defaultLayer),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* revertSvc.unrevert({ sessionID: ctx.params.sessionID })
|
|
|
})
|
|
|
|
|
|
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
|
|
params: { permissionID: PermissionID }
|
|
|
payload: typeof PermissionResponsePayload.Type
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Permission.Service.use((svc) =>
|
|
|
- svc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response }),
|
|
|
- ).pipe(Effect.provide(Permission.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response })
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
|
|
|
params: { sessionID: SessionID; messageID: MessageID }
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Effect.gen(function* () {
|
|
|
- const state = yield* SessionRunState.Service
|
|
|
- const session = yield* Session.Service
|
|
|
- yield* state.assertNotBusy(ctx.params.sessionID)
|
|
|
- yield* session.removeMessage(ctx.params)
|
|
|
- }).pipe(Effect.provide(SessionRunState.defaultLayer), Effect.provide(Session.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* runState.assertNotBusy(ctx.params.sessionID)
|
|
|
+ yield* session.removeMessage(ctx.params)
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
const deletePart = Effect.fn("SessionHttpApi.deletePart")(function* (ctx: {
|
|
|
params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
|
|
|
}) {
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Session.Service.use((svc) => svc.removePart(ctx.params)).pipe(Effect.provide(Session.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ yield* session.removePart(ctx.params)
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
@@ -524,14 +367,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
`Part mismatch: body.id='${payload.id}' vs partID='${ctx.params.partID}', body.messageID='${payload.messageID}' vs messageID='${ctx.params.messageID}', body.sessionID='${payload.sessionID}' vs sessionID='${ctx.params.sessionID}'`,
|
|
|
)
|
|
|
}
|
|
|
- const instance = yield* InstanceState.context
|
|
|
- return yield* Effect.promise(() =>
|
|
|
- Instance.restore(instance, () =>
|
|
|
- AppRuntime.runPromise(
|
|
|
- Session.Service.use((svc) => svc.updatePart(payload)).pipe(Effect.provide(Session.defaultLayer)),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
+ return yield* session.updatePart(payload)
|
|
|
})
|
|
|
|
|
|
return handlers
|