This commit is contained in:
jacob
2024-02-24 13:05:35 -06:00
28 changed files with 278 additions and 8855 deletions

View File

@@ -19,7 +19,6 @@
},
"devDependencies": {
"@types/node": "^20.1.0",
"microbundle": "^0.15.1",
"typescript": "^5.0.4"
},
"keywords": [],

View File

@@ -14,7 +14,7 @@ REST API client for managing discord application commands.
```ts
import poster from '@sern/poster';
const send = poster.client("token", "appid");
const send = await poster.client("token", "appid");
const req = await send("global/get-all", {
//options
@@ -41,6 +41,7 @@ This package is pretty simple. Create a new client and call one of the "endpoint
| guild/edit | PATCH | /applications/{application.id}/guilds/{guild.id}/commands/{command.id}|
| guild/delete | DELETE | /applications/{application.id}/guilds/{guild.id}/commands/{command.id}|
| guild/put | PUT | /applications/{application.id}/guilds/{guild.id}/commands|
| application/me | GET | /applications/@me|
Documentation for these routes are specified in the discord api documentation,
starting [here.](https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands)

View File

@@ -17,6 +17,7 @@
"author": "",
"license": "ISC",
"publishConfig": {
"access": "public"
"access": "public",
"tag": "beta"
}
}

View File

@@ -1,29 +0,0 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [published]
workflow_dispatch:
jobs:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 17
- uses: pnpm/action-setup@v2
with:
run_install: |
- recursive: true
args: [--strict-peer-dependencies]
- run: pnpm install
- run: pnpm test
- run: pnpm build
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: "public"

View File

@@ -1,13 +0,0 @@
name: release-please
on:
workflow_dispatch:
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3.1.2
with:
release-type: node
package-name: release-please-action
bump-patch-for-minor-pre-major: true

117
packages/rx/.gitignore vendored
View File

@@ -1,117 +0,0 @@
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

8
packages/rx/.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/rx.iml" filepath="$PROJECT_DIR$/rx.iml" />
</modules>
</component>
</project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,12 +0,0 @@
src
build
.editorconfig
.eslintrc.json
.gitignore
.travis.yml
rollup.config.dev.js
rollup.config.js
yarn-error.log
vitest.config.ts
test/**/*
examples

View File

@@ -1,51 +0,0 @@
# Changelog
### [0.0.3-alpha](https://github.com/sern-handler/rx/compare/v0.0.2-alpha...v0.0.3-alpha) (2023-02-15)
### Features
* add optional takeN index to toCollection ([8f6ac9f](https://github.com/sern-handler/rx/commit/8f6ac9fa20fff489eed1fcb2b01c74efad4bbee2))
* parameterize source ([feb4776](https://github.com/sern-handler/rx/commit/feb4776d0be2c71025bd18165e097f786dfc17fb))
### Bug Fixes
* generics ([51aba2a](https://github.com/sern-handler/rx/commit/51aba2a444067505b9b39cd6347c24084b733ef9))
* generics ([1d1f946](https://github.com/sern-handler/rx/commit/1d1f946cc7fbd9656acdff13f98d25de03d9ebb0))
* update package.json ([0771676](https://github.com/sern-handler/rx/commit/07716765731be34407f889ed0d58aca36c407f0a))
### [0.0.2-alpha](https://github.com/sern-handler/rx/compare/v0.0.1-alpha...v0.0.2-alpha) (2023-02-14)
### Features
* add discord.js examples ([91de33d](https://github.com/sern-handler/rx/commit/91de33dda327843caf1b7c0b77cbf42e096f8cbe))
* build for esm only ([5006f96](https://github.com/sern-handler/rx/commit/5006f96149432fa563e4db86a5fa25cf0df751d8))
* buttons ([d2ec056](https://github.com/sern-handler/rx/commit/d2ec056f4d1e4ab950266f693b385384c9cfc789))
* remove pipeline option ([baecfb6](https://github.com/sern-handler/rx/commit/baecfb6bb1de624aa2897492f54788d4d3bd4402))
* testing composable-style functions ([cd1fcf7](https://github.com/sern-handler/rx/commit/cd1fcf79825968cf1b6b75816bdc1da9add96124))
* update dependencies and tsup ([2da5fd0](https://github.com/sern-handler/rx/commit/2da5fd0b7fc360c370bae4ba4d63144977790f13))
### 0.0.1-alpha (2023-02-12)
### Features
* add more methods ([7647d92](https://github.com/sern-handler/rx/commit/7647d925865098a80859408731c55f55bb3661ca))
* add type predicates for djs bindings ([769bdf6](https://github.com/sern-handler/rx/commit/769bdf65b408acbdcb82d6851947bb05e5791c3b))
* adding scripts and some DJS bindings ([6926098](https://github.com/sern-handler/rx/commit/69260980d71a30ba5541394e420409128d4b4e9b))
* completeOnFirst works? ([4e967ed](https://github.com/sern-handler/rx/commit/4e967ed096882f612926a94bd54305f9cf093ed8))
* first commit ([0fc4004](https://github.com/sern-handler/rx/commit/0fc40048815078fc896fef5a618b7c732268cc32))
* remove redundant count operator and pause completeOnFirst ([eaeee0f](https://github.com/sern-handler/rx/commit/eaeee0fb6f0e6bde4b8a565f1cc1bfb1e5c069a7))
* update and work ([c1047a6](https://github.com/sern-handler/rx/commit/c1047a6fefb4f59f7a275db6b0d86664d5b10cdb))
### Bug Fixes
* weird thing where instanceof is not working ([eea0286](https://github.com/sern-handler/rx/commit/eea0286bcc684266cabdfc8e98cfe0e4f81afc22))
### Miscellaneous Chores
* release 0.0.1-alpha ([fd85195](https://github.com/sern-handler/rx/commit/fd85195fb3f15fcb46ebe2057f3f449a741f7944))

View File

@@ -1,7 +0,0 @@
# @sern/rx
A package extending rxjs for discord api libraries
this package offers a reactive, declarative solution using the wonderful [rxjs](https://github.com/ReactiveX/rxjs) to the current collectors
and listeners in many discord bot libraries. It's still in alpha, but most operators and functions have been tested using vitest.
- View [examples](https://github.com/sern-handler/rx/tree/main/examples) to get started!

View File

@@ -1,18 +0,0 @@
import {composable, useMutableState} from "../src/index";
import {fromEvent} from "rxjs";
import {EventEmitter} from "events";
//hypothetical EventEmitter
declare const ee : EventEmitter
const [str, setData, manager] = useMutableState("root")
const messageCreate = fromEvent(ee, 'messageCreate')
composable<string>((close, message) => {
if(message === "!ping") {
setData(message)
}
},[messageCreate])

View File

@@ -1,39 +0,0 @@
import {ButtonInteraction, ChatInputCommandInteraction, Collection, ComponentType} from "discord.js";
import {filter, firstValueFrom, take} from 'rxjs'
import {asyncTask, DJS, on, once, time} from "../../src/index.js";
import {clientEvent, matchesCustomId} from "../../src/djs";
//Hypothetical interaction source
declare const ctx : ChatInputCommandInteraction
// create a collector that accepts distinct interactions.
// A timer is set to 5 seconds and it will emit one collection where its key is the interaction's id and value the interaction.
const reactiveCollector = on<ButtonInteraction>('collect', ctx.channel.createMessageComponentCollector({ componentType: ComponentType.Button }))
.pipe(
DJS.distinctCustomId,
time(5000),
DJS.toCollection(src => [src.id, src], 4),
)
//Collection as promise
const promisedCollection = firstValueFrom(reactiveCollector, { defaultValue : new Collection() })
//Collection subscription
const subscriptionCollection = reactiveCollector.subscribe({
next: (collection) => console.log(collection),
error: (err) => console.log(err),
complete: () => console.log("The collector has finished") //synonymous to the 'end' event for basic discord.js collectors!
})
//Creates a base button handler that matches the given customId
const buttonHandler = clientEvent(ctx.client, 'interactionCreate')
.pipe(
filter(DJS.isButton), matchesCustomId("i-miss-her"),
)
//From the buttonHandler, defer the update and set a timeout of 60_000. Then it executes once, finally closing the subscription
buttonHandler.pipe(
asyncTask( b => b.deferUpdate()),
time(60_000 ), // 1 minute
once((b) => b.editReply(`You clicked a button once`))
).subscribe()

View File

@@ -1,50 +0,0 @@
{
"name": "@sern/rx",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs"
},
"./djs": {
"import": "./dist/djs/index.mjs"
}
},
"version": "0.0.3-alpha",
"description": "rxjs operators and utils for discord bot projects",
"packageManager": "pnpm@7.24.3",
"scripts": {
"build": "tsup",
"tdd": "vitest --config ./vitest.config.ts",
"test": "vitest run --config ./vitest.config.ts"
},
"keywords": [],
"author": "jacoobes",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.11.15",
"discord.js": "^14.8.1",
"rxjs": "7.8.1",
"tsup": "^6.7.0",
"typescript": "5.0.4",
"vitest": "^0.28.4"
},
"peerDependencies": {
"discord.js": ">= 14.8.0"
},
"node": ">=18.0",
"type": "module",
"types": "./dist\\index.d.ts",
"directories": {
"example": "examples",
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/rx.git"
},
"bugs": {
"url": "https://github.com/sern-handler/rx/issues"
},
"homepage": "https://github.com/sern-handler/rx#readme"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,95 +0,0 @@
import type {
AnySelectMenuInteraction,
ButtonInteraction, ChannelSelectMenuInteraction, ClientEvents,
MentionableSelectMenuInteraction,
ModalSubmitInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserSelectMenuInteraction,
} from 'discord.js'
import { Collection } from "discord.js";
import {
distinctUntilKeyChanged,
filter,
MonoTypeOperatorFunction,
Observable,
OperatorFunction,
pipe,
reduce,
take
} from "rxjs";
import type {Client} from "discord.js";
import {CustomId, on} from "../index.js";
/**
* discord.js binding to ensure an event is {@link ButtonInteraction}
* @param e
*/
export function isButton(e: unknown): e is ButtonInteraction {
return (e as ButtonInteraction)?.isButton?.() ?? false
}
export function matchesCustomId<T extends CustomId>(cmpt: string) : MonoTypeOperatorFunction<T> {
return filter(i => i.customId == cmpt)
}
/**
* discord.js binding to ensure an event is {@link AnySelectMenuInteraction}
* Reminder that AnySelectMenuInteraction will be renamed to SelectMenuInteraction in the next major
* @param e
*/
export function isSelectMenu(e: unknown): e is AnySelectMenuInteraction {
return (e as AnySelectMenuInteraction)?.isAnySelectMenu?.() ?? false
}
export function isRoleSelectMenu(e: unknown): e is RoleSelectMenuInteraction {
return (e as RoleSelectMenuInteraction)?.isRoleSelectMenu?.() ?? false
}
export function isStringSelectMenu(e: unknown): e is StringSelectMenuInteraction {
return (e as StringSelectMenuInteraction)?.isStringSelectMenu?.() ?? false
}
export function isChannelSelectMenu(e: unknown): e is ChannelSelectMenuInteraction {
return (e as ChannelSelectMenuInteraction)?.isChannelSelectMenu?.() ?? false
}
export function isUserSelectMenu(e: unknown): e is UserSelectMenuInteraction {
return (e as UserSelectMenuInteraction)?.isUserSelectMenu?.() ?? false;
}
export function isMentionableSelectMenu(e: unknown) : e is MentionableSelectMenuInteraction {
return (e as MentionableSelectMenuInteraction)?.isMentionableSelectMenu?.() ?? false;
}
export function isModal(e: unknown) : e is ModalSubmitInteraction {
return (e as ModalSubmitInteraction)?.isModalSubmit?.() ?? false;
}
/**
* emits source stream only if the custom id is unique
*/
export const distinctCustomId = <T extends { customId: string }> (src$: Observable<T>) => src$.pipe(distinctUntilKeyChanged('customId'));
//Collects all source emissions and emits them as a discord.js Collection when the source completes.
export function toCollection<Source, Key, Value>(
transform: (src : Source) => [Key, Value],
takeN?: number
): OperatorFunction<Source, Collection<Key,Value>> {
return pipe(
takeN ? take(takeN) : pipe(),
reduce((coll, src) => {
return coll.set(...transform(src));
}, new Collection<Key, Value>())
)
}
/**
* Creates a typed client event observable
* @param c
* @param key
*/
export function clientEvent<T extends keyof ClientEvents>(c : Client, key : T) {
return on(key, c) as Observable<ClientEvents[T]>
}

View File

@@ -1,196 +0,0 @@
import type {EventEmitter} from "events";
import {
BehaviorSubject,
defer,
fromEvent,
mergeMap,
type MonoTypeOperatorFunction,
Observable,
Subject, switchMap,
take,
takeUntil,
tap,
timer,
concatMap,
pipe,
catchError
} from "rxjs";
export * as DJS from './djs/index.js'
export interface CustomId {
customId: string;
}
/**
* Creates an observable using fromEvent & casts it to Observable<O>
* @param name
* @param e
*/
export function on<O>(name: string, e: EventEmitter): Observable<O> {
return fromEvent(e, name) as Observable<O>;
}
/**
* Do some task that requires asynchronous await API and reedits the source
* @param cb
*/
export function asyncTask<Event>(cb: (e: Event) => Promise<unknown>) {
return (src$: Observable<Event>) => src$.pipe(switchMap(cb), mergeMap(() => src$))
}
/**
* the operator function takeUntil with a timer. Completes the source stream after time has been reached
* @param time a Date object or number (milliseconds)
*/
export function time<Event>(time: Date | number) {
return takeUntil(timer(time)) as MonoTypeOperatorFunction<Event>
}
/**
* @param action
* Responds to one interaction and calls { action }.
* Unsubscribes and closes subscription afterwards
*/
export function once<Event>(action: (e: Event) => unknown) {
return (source$: Observable<Event>) =>
defer(() => {
let isFirst = true;
return source$.pipe(
tap(v => {
if (isFirst) {
action(v);
isFirst = false;
}
}),
take(1)
);
});
}
/**
* @Experimental - This api may be moved to another package, deleted, or anything. consider it for playing around only
* The scope which a composable function acts on.
*/
class ComposableScope<Source> {
private symbol = Symbol("nothing");
constructor(
private updater: Subject<Source>,
private listeners: (BehaviorSubject<unknown> | Observable<unknown>)[]
) {}
private executeIfUpdaterNotClosed(action: () => any) {
if (!this.updater.closed) {
action()
}
}
listen(recomposable: (close: () => void, source: Source) => unknown) {
const updaterFinalizer = () => this.updater.complete();
this.updater
.subscribe({
next: data => recomposable(updaterFinalizer, data),
complete: () => {
this.listeners.forEach(s => {
if (s instanceof BehaviorSubject) {
s.unsubscribe()
}
})
},
error: () => {
this.listeners.forEach(s => {
if (s instanceof BehaviorSubject) {
s.unsubscribe()
}
})
}
})
if (this.listeners.length === 0) {
this.updater.complete()
} else {
for (const subject of this.listeners) {
subject.subscribe((data) => {
if (subject instanceof Observable) {
this.executeIfUpdaterNotClosed(() => {
this.updater.next(data as Source)
})
} else {
this.executeIfUpdaterNotClosed(() => {
this.updater.next(this.symbol as Source)
})
}
})
}
}
}
}
/**
* @Experimental - This api may be moved to another package, deleted, or anything. consider it for playing around only
* @param seed - The beginning value. This works similarly to react useState or compose remember mutableStateOf
*/
export function useMutableState<T>(seed: T) {
const stateManager = new BehaviorSubject(seed)
return [
() => stateManager.getValue(),
(vl: T) => stateManager.next(vl),
stateManager,
] as const
}
export type Listeners = (BehaviorSubject<unknown> | Observable<unknown>)[]
/**
* @Experimental - This api may be moved to another package, deleted, or anything. consider it for playing around only
* Creates a scope that can executed as many times as needed, provided it is listening to data.
* @param scope - Executes callback until
* @param listeners
*/
export function composable<Source>(
scope: (close: () => void, source: Source) => unknown,
listeners: Listeners
) {
const composableScope = new ComposableScope<Source>(new Subject(), listeners);
composableScope.listen(scope)
}
export function filterMap<I, O>(cb: (i: I) => Observable<O> | Observable<never>) {
return concatMap(cb)
}
type StreamResult =
| { crash: true, error: Error }
| { crash: false, error?: never }
export function failableStream<I, O(
pipeline: typeof pipe,
ehandler: (e: any) => StreamResult
) {
return pipe(
pipeline,
catchError((err, caught) => {
const { crash, error } = ehandler(err)
if(crash) {
throw error
} else {
return caught;
}
})
);
}
//
// /**
// *
// * @param notifiers Should be terminal operators. Meaning, these should have some logic to close its subscription
// */
// export function completeOnFirst<Event>(notifiers: MonoTypeOperatorFunction<Event>[]) {
// return (src: Observable<Event>) =>
// src.pipe(raceWith(notifiers.map(notif => src.pipe(notif))));
// }
//
//

View File

@@ -1,100 +0,0 @@
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
import {composable, once, time, useMutableState} from '../src/index'
import {of, from } from 'rxjs'
describe('sern/rx common operators', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach( () => {
vi.restoreAllMocks()
})
it("subscription completes after 2 seconds", () => {
const completion = vi.fn(() => void 0)
of("x").pipe(time(5_000)).subscribe({
complete: () => {
completion()
}
})
vi.advanceTimersByTime(2000)
expect(completion).toHaveBeenCalled()
});
it("should call once and close", () => {
const tapOnce = vi.fn((a : number) => a)
const sub = from([1,2,3,4,5]).pipe(once(tapOnce)).subscribe()
expect(tapOnce).toHaveBeenCalledOnce()
expect(sub.closed).toBe(true)
})
})
describe("composable", () => {
afterEach(() => {
vi.restoreAllMocks()
})
it("should close without calling unsubscribe manually", () => {
const [data, setData, manager] = useMutableState("shiddd")
composable((close) => {
if(data() === "shiddd") {
setData("pooo")
} else close()
}, [manager]);
expect(manager.closed).toBe(true)
})
it("should invoke size of string's length", () => {
let invoke = 0
const theString = "shiddd"
const [data, setData, manager] = useMutableState(theString)
composable((close) => {
const str = data()
if(str.length != 0) {
invoke++
setData(str.slice(0, str.length-1))
} else {
close()
}
}, [manager]);
expect(manager.closed).toBe(true)
expect(invoke).toBe(theString.length)
})
it("should act as an Observable stream", () => {
let invoke = 0
composable(() => {
invoke++
}, [from([1,2,3,4,5])])
expect(invoke).toBe(5)
})
it("should concat a string based on event", () => {
const [data, setData] = useMutableState("")
composable((_, source) => {
setData(data() + source)
}, [from([1,2,3,4,5])])
expect(data()).toBe("12345")
})
})
//
// describe("completeFirst", () => {
//
// it("should invoke twice", () => {
// let invoke = 0
// from([1,2,3,4,5]).pipe(
// completeOnFirst([take(2)])
// ).subscribe((s) => {
// console.log(s)
// invoke++
// })
//
// expect(invoke).toBe(2)
//
// })
// })

View File

@@ -1,188 +0,0 @@
import {ButtonInteraction, ComponentType, InteractionType, ModalSubmitInteraction } from 'discord.js'
import { filter, from, of } from 'rxjs'
import {beforeEach, describe, expect, it, vi} from 'vitest'
import { DJS } from '../src/index'
import {clientEvent, distinctCustomId, matchesCustomId, toCollection} from "../src/djs";
import {EventEmitter} from "events";
import {fail} from "assert";
import exp from "constants";
vi.mock('discord.js', () => {
const Collection = Map
const ModalSubmitInteraction = class {
customId
type = 5
isModalSubmit = vi.fn()
constructor(customId) {
this.customId = customId
}
}
const ButtonInteraction = class {
customId
type = 3
componentType = 2
isButton = vi.fn()
constructor(customId) {
this.customId = customId
}
}
return {
Collection,
ComponentType: {
Button: 2
},
InteractionType : {
MessageComponent : 3,
ModalSubmit : 5
},
ModalSubmitInteraction,
ButtonInteraction
};
})
describe("test isButton ", () => {
let btnInteraction
beforeEach( () => {
btnInteraction = new ButtonInteraction("hello")
})
afterEach(() => vi.clearAllMocks())
it("should invoke isButton 0 times", () => {
let invoked = 0;
from("not a discord button interaction").pipe(
filter(DJS.isButton)
).subscribe(() => invoked++)
expect(invoked).toBe(0)
});
it("should invoke isButton once", () => {
let invoked2 = 0;
btnInteraction.isButton.mockReturnValue(
btnInteraction.componentType === ComponentType.Button
&& btnInteraction.type === InteractionType.MessageComponent
)
of(btnInteraction).pipe(
filter(DJS.isButton)
).subscribe(() => invoked2++)
expect(invoked2).toBe(1)
})
})
describe("match customid operator", () => {
let buttonInteraction
beforeEach(() => {
buttonInteraction = new ButtonInteraction("hello-world")
buttonInteraction.isButton.mockReturnValue(true)
})
afterEach(() => {
vi.clearAllMocks()
})
it("shouldn't invoke", () => {
let invoked = 0
of(buttonInteraction).pipe(
filter(DJS.isButton),
matchesCustomId("pooba")
).subscribe(() => invoked++)
expect(invoked).toBe(0)
})
it("should invoke once", () => {
let invoked = 0
of(buttonInteraction).pipe(
filter(DJS.isButton),
matchesCustomId("hello-world")
).subscribe(() => invoked++)
expect(invoked).toBe(1)
})
})
describe("toCollection operator", () => {
it("should fill 10 elements", () => {
const expectedMap = new Map([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10]
])
let coll
from([1,2,3,4,5,6,7,8,9,10]).pipe(
toCollection(src => [src, src])
).subscribe(collection => {
coll = collection
})
expect(coll).toEqual(expectedMap)
})
})
describe("distinct custom id operator", () => {
it("should invoke once", () => {
let invoked = 0;
from([new ButtonInteraction("p1"),new ButtonInteraction("p1"),new ButtonInteraction("p1") ])
.pipe(distinctCustomId)
.subscribe(() => invoked++)
expect(invoked).toBe(1)
})
it("should invoke twice", () => {
let invoked = 0;
from([new ButtonInteraction("p1"),new ButtonInteraction("p1"),new ButtonInteraction("p2") ])
.pipe(distinctCustomId)
.subscribe(() => invoked++)
expect(invoked).toBe(2)
})
})
describe("isModal predicate", () => {
afterEach(() => {
vi.clearAllMocks()
})
it("should invoke once", () => {
let invoked = 0
const m = new ModalSubmitInteraction("p1")
m.isModalSubmit.mockReturnValue(m.type === InteractionType.ModalSubmit)
const b = new ButtonInteraction("p2")
b.isButton.mockReturnValue(b.type === InteractionType.MessageComponent && b.componentType === ComponentType.Button)
from([m, b])
.pipe(filter(DJS.isModal))
.subscribe(() => invoked++)
expect(invoked).toBe(1)
})
})
describe("clientEvent observable generator", () => {
let c
let cEvent
let btnClickFromClientEvent
beforeEach(() => {
c = new EventEmitter()
cEvent = clientEvent(c, 'interactionCreate').subscribe((b) =>btnClickFromClientEvent = b )
})
afterEach( () => {
vi.clearAllMocks()
cEvent.unsubscribe()
})
it("should yield ButtonInteraction ", () => {
const btnClick = new ButtonInteraction("p1")
c.emit('interactionCreate', btnClick)
expect(btnClick).toBe(btnClickFromClientEvent)
})
it("should not emit correctly", () => {
const btnClick = new ButtonInteraction("p1")
const modalSubmit = new ModalSubmitInteraction("p1")
c.emit('interactionCreate', modalSubmit)
expect(btnClickFromClientEvent).not.toBe(btnClick)
})
})

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "dist"],
"include": ["src"]
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"target": "esnext"
}
}

View File

@@ -1,25 +0,0 @@
import { defineConfig } from 'tsup';
const shared = {
entry: ['src/index.ts', 'src/djs/index.ts'],
external: ['discord.js', 'rxjs'],
platform: 'node',
clean: true,
sourcemap: false,
};
export default defineConfig([
{
format: 'esm',
target: 'node17',
tsconfig: './tsconfig-esm.json',
dts: true,
outDir: './dist',
external: ['discord.js'],
treeshake: true,
outExtension() {
return {
js: '.mjs',
};
},
...shared,
},
]);

View File

@@ -1,9 +0,0 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
includeSource: ["src/**/*.{js,ts}"]
},
})