Skip to content

Commit

Permalink
feat: error support for InteractionBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim Smart committed Dec 3, 2022
1 parent 77a6acb commit 3747f96
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 52 deletions.
38 changes: 31 additions & 7 deletions src/DiscordGateway/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,56 @@
import { filter } from "callbag-effect-ts/Source"
import { spawn } from "./Sharder/index.js"

export const makeFromDispatch =
export const fromDispatchFactory =
<R, E>(
source: EffectSource<R, E, Discord.GatewayPayload<Discord.ReceiveEvent>>,
) =>
<K extends keyof Discord.ReceiveEvents>(
event: K,
): EffectSource<R, E, Discord.ReceiveEvents[K]> =>
pipe(
source,
filter((p) => p.t === event),
).map((p) => p.d! as any)
source.filter((p) => p.t === event).map((p) => p.d! as any)

export const handleDispatchFactory =
<R, E>(
source: EffectSource<R, E, Discord.GatewayPayload<Discord.ReceiveEvent>>,
) =>
<K extends keyof Discord.ReceiveEvents, R1, E1, A>(
event: K,
handle: (event: Discord.ReceiveEvents[K]) => Effect<R1, E1, A>,
): Effect<R | R1, E | E1, void> =>
source
.filter((p) => p.t === event)
.map((p) => p.d! as Discord.ReceiveEvents[K])
.chainPar((a) => EffectSource.fromEffect(handle(a))).runDrain

export const make = Do(($) => {
const shards = $(spawn.share)
const raw = $(shards.chainPar((s) => s.raw).share)
const dispatch = $(shards.chainPar((s) => s.dispatch).share)
const fromDispatch = makeFromDispatch(dispatch)
const fromDispatch = fromDispatchFactory(dispatch)
const handleDispatch = handleDispatchFactory(dispatch)

return {
shards,
raw,
dispatch,
fromDispatch,
handleDispatch,
}
})

export interface DiscordGateway extends Success<typeof make> {}
export const DiscordGateway = Tag<DiscordGateway>()
export const LiveDiscordGateway = Layer.fromEffect(DiscordGateway)(make)

export const handleDispatch = <
K extends keyof Discord.ReceiveEvents,
R1,
E1,
A,
>(
event: K,
handle: (event: Discord.ReceiveEvents[K]) => Effect<R1, E1, A>,
) =>
Effect.serviceWithEffect(DiscordGateway)((a) =>
a.handleDispatch(event, handle),
)
64 changes: 37 additions & 27 deletions src/Interactions/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,126 +10,136 @@ type Interaction<Type, Data, HasMessage = false> = Omit<
}
: {})

export type InteractionHandler<R, Type, Data, HasMessage = false> = (
export type InteractionHandler<R, E, Type, Data, HasMessage = false> = (
a: Interaction<Type, Data, HasMessage>,
) => Effect<R, never, void>
) => Effect<R, E, void>

export type InteractionDefinition<R> =
| GlobalApplicationCommand<R>
| GuildApplicationCommand<R>
| MessageComponent<R>
| ModalSubmit<R>
| Autocomplete<R>
export type InteractionDefinition<R, E> =
| GlobalApplicationCommand<R, E>
| GuildApplicationCommand<R, E>
| MessageComponent<R, E>
| ModalSubmit<R, E>
| Autocomplete<R, E>

export class GlobalApplicationCommand<R> {
export class GlobalApplicationCommand<R, E> {
readonly _tag = "GlobalApplicationCommand"
constructor(
readonly command: Discord.CreateGlobalApplicationCommandParams,
readonly handle: InteractionHandler<
R,
E,
Discord.InteractionType.APPLICATION_COMMAND,
Discord.ApplicationCommandDatum
>,
) {}
}

export const global = <R>(
export const global = <R, E>(
command: Discord.CreateGlobalApplicationCommandParams,
handle: InteractionHandler<
R,
E,
Discord.InteractionType.APPLICATION_COMMAND,
Discord.ApplicationCommandDatum
>,
) => new GlobalApplicationCommand(command, handle)

export class GuildApplicationCommand<R> {
export class GuildApplicationCommand<R, E> {
readonly _tag = "GuildApplicationCommand"
constructor(
readonly command: Discord.CreateGlobalApplicationCommandParams,
readonly handle: InteractionHandler<
R,
E,
Discord.InteractionType.APPLICATION_COMMAND,
Discord.ApplicationCommandDatum
>,
) {}
}

export const guild = <R>(
export const guild = <R, E>(
command: Discord.CreateGuildApplicationCommandParams,
handle: InteractionHandler<
R,
E,
Discord.InteractionType.APPLICATION_COMMAND,
Discord.ApplicationCommandDatum
>,
) => new GuildApplicationCommand(command, handle)

export class MessageComponent<R> {
export class MessageComponent<R, E> {
readonly _tag = "MessageComponent"
constructor(
readonly predicate: (customId: string) => Effect<R, never, boolean>,
readonly predicate: (customId: string) => Effect<R, E, boolean>,
readonly handle: InteractionHandler<
R,
E,
Discord.InteractionType.MESSAGE_COMPONENT,
Discord.MessageComponentDatum,
true
>,
) {}
}

export const messageComponent = <R1, R2>(
pred: (customId: string) => Effect<R1, never, boolean>,
export const messageComponent = <R1, R2, E1, E2>(
pred: (customId: string) => Effect<R1, E1, boolean>,
handle: InteractionHandler<
R2,
E2,
Discord.InteractionType.MESSAGE_COMPONENT,
Discord.MessageComponentDatum,
true
>,
) => new MessageComponent<R1 | R2>(pred, handle)
) => new MessageComponent<R1 | R2, E1 | E2>(pred, handle)

export class ModalSubmit<R> {
export class ModalSubmit<R, E> {
readonly _tag = "ModalSubmit"
constructor(
readonly predicate: (customId: string) => Effect<R, never, boolean>,
readonly predicate: (customId: string) => Effect<R, E, boolean>,
readonly handle: InteractionHandler<
R,
E,
Discord.InteractionType.MODAL_SUBMIT,
Discord.ModalSubmitDatum,
true
>,
) {}
}

export const modalSubmit = <R1, R2>(
pred: (customId: string) => Effect<R1, never, boolean>,
export const modalSubmit = <R1, R2, E1, E2>(
pred: (customId: string) => Effect<R1, E1, boolean>,
handle: InteractionHandler<
R2,
E2,
Discord.InteractionType.MODAL_SUBMIT,
Discord.ModalSubmitDatum,
true
>,
) => new ModalSubmit<R1 | R2>(pred, handle)
) => new ModalSubmit<R1 | R2, E1 | E2>(pred, handle)

export class Autocomplete<R> {
export class Autocomplete<R, E> {
readonly _tag = "Autocomplete"
constructor(
readonly predicate: (
focusedOption: Discord.ApplicationCommandInteractionDataOption,
) => Effect<R, never, boolean>,
) => Effect<R, E, boolean>,
readonly handle: InteractionHandler<
R,
E,
Discord.InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE,
Discord.ApplicationCommandDatum
>,
) {}
}

export const autocomplete = <R1, R2>(
export const autocomplete = <R1, R2, E1, E2>(
pred: (
focusedOption: Discord.ApplicationCommandInteractionDataOption,
) => Effect<R1, never, boolean>,
) => Effect<R1, E1, boolean>,
handle: InteractionHandler<
R2,
E2,
Discord.InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE,
Discord.ApplicationCommandDatum
>,
) => new Autocomplete<R1 | R2>(pred, handle)
) => new Autocomplete<R1 | R2, E1 | E2>(pred, handle)
35 changes: 17 additions & 18 deletions src/Interactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,45 @@ export {
InteractionDefinition,
} from "./definitions.js"

export const builder = () => new InteractionBuilder<never>([])
export const builder = () => new InteractionBuilder<never, never>([])

class InteractionBuilder<R> {
constructor(readonly definitions: D.InteractionDefinition<R>[]) {}
class InteractionBuilder<R, E> {
constructor(readonly definitions: D.InteractionDefinition<R, E>[]) {}

add<R1>(definition: D.InteractionDefinition<R1>) {
return new InteractionBuilder<R | R1>([...this.definitions, definition])
add<R1, E1>(definition: D.InteractionDefinition<R1, E1>) {
return new InteractionBuilder<R | R1, E | E1>([
...this.definitions,
definition,
])
}

get run() {
return runGateway(this.definitions)
}
}

const runGateway = <R>(definitions: D.InteractionDefinition<R>[]) =>
const runGateway = <R, E>(definitions: D.InteractionDefinition<R, E>[]) =>
Do(($) => {
const globalCommands = definitions.filter(
(a): a is D.GlobalApplicationCommand<R> =>
(a): a is D.GlobalApplicationCommand<R, E> =>
a._tag === "GlobalApplicationCommand",
)

const guildCommands = definitions.filter(
(a): a is D.GuildApplicationCommand<R> =>
(a): a is D.GuildApplicationCommand<R, E> =>
a._tag === "GuildApplicationCommand",
)

const messageComponents = definitions.filter(
(a): a is D.MessageComponent<R> => a._tag === "MessageComponent",
(a): a is D.MessageComponent<R, E> => a._tag === "MessageComponent",
)

const modalSubmits = definitions.filter(
(a): a is D.ModalSubmit<R> => a._tag === "ModalSubmit",
(a): a is D.ModalSubmit<R, E> => a._tag === "ModalSubmit",
)

const autocompletes = definitions.filter(
(a): a is D.Autocomplete<R> => a._tag === "Autocomplete",
(a): a is D.Autocomplete<R, E> => a._tag === "Autocomplete",
)

const application = $(
Expand All @@ -58,20 +61,18 @@ const runGateway = <R>(definitions: D.InteractionDefinition<R>[]) =>
}),
)

const gateway = $(Effect.service(Gateway.DiscordGateway))

const allCommands = [...globalCommands, ...guildCommands].reduce(
(acc, a) => ({
...acc,
[a.command.name]: a,
}),
{} as Record<
string,
D.GlobalApplicationCommand<R> | D.GuildApplicationCommand<R>
D.GlobalApplicationCommand<R, E> | D.GuildApplicationCommand<R, E>
>,
)

const handle = (i: Discord.Interaction): Effect<R, never, void> => {
const handle = (i: Discord.Interaction): Effect<R, E, void> => {
switch (i.type) {
case Discord.InteractionType.APPLICATION_COMMAND:
const data = i.data as Discord.ApplicationCommandDatum
Expand Down Expand Up @@ -102,9 +103,7 @@ const runGateway = <R>(definitions: D.InteractionDefinition<R>[]) =>
return Effect.unit()
}

const run = gateway
.fromDispatch("INTERACTION_CREATE")
.chainPar((i) => EffectSource.fromEffect(handle(i))).runDrain
const run = Gateway.handleDispatch("INTERACTION_CREATE", handle)

$(run)
})
Expand Down

0 comments on commit 3747f96

Please sign in to comment.