diff --git a/src/Client.ts b/src/Client.ts index 41a7f2da..96eb3589 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -9,23 +9,38 @@ import messageClassFactory from "./classes/Message"; import serverClassFactory from "./classes/Server"; import serverMemberClassFactory from "./classes/ServerMember"; import userClassFactory from "./classes/User"; +import { + ChannelCollection, + EmojiCollection, + MessageCollection, + ServerCollection, + ServerMemberCollection, + UserCollection, +} from "./collections"; import { EventClient, createEventClient } from "./events/client"; // eslint-disable-next-line -type Obj any> = InstanceType>; +type O any> = InstanceType>; -export type Channel = Obj; -export type Emoji = Obj; -export type Message = Obj; -export type Server = Obj; -export type ServerMember = Obj; -export type User = Obj; +export type Channel = O; +export type Emoji = O; +export type Message = O; +export type Server = O; +export type ServerMember = O; +export type User = O; export type Session = { token: string; user_id: string } | string; /** * Revolt.js Client */ export class Client { + readonly Channel: ReturnType; + readonly Emoji: ReturnType; + readonly Message: ReturnType; + readonly User: ReturnType; + readonly Server: ReturnType; + readonly ServerMember: ReturnType; + readonly channels; readonly emojis; readonly messages; @@ -59,12 +74,22 @@ export class Client { this.ready = ready; this.#setReady = setReady; - this.channels = channelClassFactory(this); - this.emojis = emojiClassFactory(this); - this.messages = messageClassFactory(this); - this.users = userClassFactory(this); - this.servers = serverClassFactory(this); - this.serverMembers = serverMemberClassFactory(this); + this.channels = new ChannelCollection(); + this.emojis = new EmojiCollection(); + this.messages = new MessageCollection(); + this.users = new UserCollection(); + this.servers = new ServerCollection(); + this.serverMembers = new ServerMemberCollection(); + + this.Channel = channelClassFactory(this, this.channels as never); + this.Emoji = emojiClassFactory(this, this.emojis as never); + this.Message = messageClassFactory(this, this.messages as never); + this.User = userClassFactory(this, this.users as never); + this.Server = serverClassFactory(this, this.servers as never); + this.ServerMember = serverMemberClassFactory( + this, + this.serverMembers as never + ); } /** @@ -76,7 +101,7 @@ export class Client { if (event.type === "Ready") { console.time("load users"); for (const user of event.users) { - const u = new this.users(user._id, user); + const u = new this.User(user._id, user); if (u.relationship === "User") { this.user = u; @@ -85,22 +110,22 @@ export class Client { console.timeEnd("load users"); console.time("load servers"); for (const server of event.servers) { - new this.servers(server._id, server); + new this.Server(server._id, server); } console.timeEnd("load servers"); console.time("load memberships"); for (const member of event.members) { - new this.serverMembers(member._id, member); + new this.ServerMember(member._id, member); } console.timeEnd("load memberships"); console.time("load channels"); for (const channel of event.channels) { - new this.channels(channel._id, channel); + new this.Channel(channel._id, channel); } console.timeEnd("load channels"); console.time("load emojis"); for (const emoji of event.emojis) { - new this.emojis(emoji._id, emoji); + new this.Emoji(emoji._id, emoji); } console.timeEnd("load emojis"); @@ -113,11 +138,11 @@ export class Client { "It has the channels:", lounge.channels.map((channel) => channel.name) ); - console.log( + /*console.log( "They joined at:", this.serverMembers.get({ server: lounge.id, user: lounge.owner!.id }) ?.joinedAt - ); + );*/ this.#setReady(true); } diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index 8bb69448..58b2a415 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -1,107 +1,60 @@ -import { ReactiveMap } from "@solid-primitives/map"; -import type { Channel as ApiChannel } from "revolt-api"; -import { decodeTime } from "ulid"; +import { SetStoreFunction } from "solid-js/store"; + +import type { + Channel as ApiChannel, + DataEditChannel, + DataMessageSend, + Member, + Message, + User, +} from "revolt-api"; +import { APIRoutes } from "revolt-api/dist/routes"; +import { decodeTime, ulid } from "ulid"; import { Client } from "../Client"; +import { StoreCollection } from "../collections/Collection"; import { hydrate } from "../hydration"; import { HydratedChannel } from "../hydration/channel"; import { bitwiseAndEq, calculatePermission } from "../permissions/calculator"; import { Permission } from "../permissions/definitions"; -import { ObjectStorage } from "../storage/ObjectStorage"; -export default (client: Client) => +export default ( + client: Client, + collection: StoreCollection +) => /** * Channel Class */ class Channel { - static #storage = new ObjectStorage(); + static #collection: StoreCollection< + InstanceType, + HydratedChannel + >; + static #set: SetStoreFunction>; + static #get: (id: string) => HydratedChannel; static { + Channel.#collection = collection as never; + Channel.#set = collection.updateUnderlyingObject as never; + Channel.#get = collection.getUnderlyingObject as never; + client.events.on("event", (event) => { switch (event.type) { case "ChannelUpdate": { - this.#storage.set(event.id, hydrate("channel", event.data)); + this.#set(event.id, hydrate("channel", event.data)); break; } } }); } - // * Object Map Definition - static #objects = new ReactiveMap>(); - - /** - * Get an existing object - * @param id ID - * @returns Object - */ - static get(id: string): InstanceType | undefined { - return this.#objects.get(id); - } - - /** - * Number of stored objects - * @returns Size - */ - static size() { - return this.#objects.size; - } - - /** - * Iterable of keys in the map - * @returns Iterable - */ - static keys() { - return this.#objects.keys(); - } - - /** - * Iterable of values in the map - * @returns Iterable - */ - static values() { - return this.#objects.values(); - } - - /** - * List of values in the map - * @returns List - */ - static toList() { - return [...this.#objects.values()]; - } - - /** - * Iterable of key, value pairs in the map - * @returns Iterable - */ - static entries() { - return this.#objects.entries(); - } - - /** - * Execute a provided function over each key, value pair in the map - * @param cb Callback for each pair - * @returns Iterable - */ - static forEach( - cb: ( - value: InstanceType, - key: string, - map: ReactiveMap> - ) => void - ) { - return this.#objects.forEach(cb); - } - // * End Object Map Definition - /** * Fetch channel by ID * @param id ID * @returns Channel */ static async fetch(id: string): Promise { - const channel = Channel.get(id); + const channel = Channel.#collection.get(id); if (channel) return channel; const data = await client.api.get(`/channels/${id as ""}`); @@ -116,12 +69,16 @@ export default (client: Client) => */ constructor(id: string, data?: ApiChannel) { this.id = id; - Channel.#storage.hydrate(id, "channel", data); - Channel.#objects.set(id, this); + Channel.#collection.create(id, "channel", this, data); } - updateSomething() { - Channel.#storage.set(this.id, "name", "troling"); + /** + * Get or create + * @param id Id + * @param data Data + */ + static new(id: string, data?: ApiChannel) { + return client.channels.get(id) ?? new Channel(id, data); } /** @@ -135,51 +92,51 @@ export default (client: Client) => * Channel type */ get type() { - return Channel.#storage.get(this.id).channelType; + return Channel.#get(this.id).channelType; } /** * Channel name */ get name() { - return Channel.#storage.get(this.id).name; + return Channel.#get(this.id).name; } /** * Channel description */ get description() { - return Channel.#storage.get(this.id).description; + return Channel.#get(this.id).description; } /** * Channel icon */ get icon() { - return Channel.#storage.get(this.id).icon; + return Channel.#get(this.id).icon; } /** * Whether the conversation is active */ get active() { - return Channel.#storage.get(this.id).active; + return Channel.#get(this.id).active; } /** * User Ids of recipients of the group */ get recipientIds() { - return Channel.#storage.get(this.id).recipientIds; + return Channel.#get(this.id).recipientIds; } /** * Recipients of the group */ get recipients() { - return Channel.#storage - .get(this.id) - .recipientIds.map((id) => client.users.get(id)!); + return Channel.#get(this.id).recipientIds.map( + (id) => client.users.get(id)! + ); } /** @@ -195,77 +152,77 @@ export default (client: Client) => * User ID */ get userId() { - return Channel.#storage.get(this.id).userId!; + return Channel.#get(this.id).userId!; } /** * User this channel belongs to */ get user() { - return client.users.get(Channel.#storage.get(this.id).userId!); + return client.users.get(Channel.#get(this.id).userId!); } /** * Owner ID */ get ownerId() { - return Channel.#storage.get(this.id).ownerId!; + return Channel.#get(this.id).ownerId!; } /** * Owner of the group */ get owner() { - return client.users.get(Channel.#storage.get(this.id).ownerId!); + return client.users.get(Channel.#get(this.id).ownerId!); } /** * Server ID */ get serverId() { - return Channel.#storage.get(this.id).serverId!; + return Channel.#get(this.id).serverId!; } /** * Server this channel is in */ get server() { - return client.servers.get(Channel.#storage.get(this.id).serverId!); + return client.servers.get(Channel.#get(this.id).serverId!); } /** * Permissions allowed for users in this group */ get permissions() { - return Channel.#storage.get(this.id).permissions; + return Channel.#get(this.id).permissions; } /** * Default permissions for this server channel */ get defaultPermissions() { - return Channel.#storage.get(this.id).defaultPermissions; + return Channel.#get(this.id).defaultPermissions; } /** * Role permissions for this server channel */ get rolePermissions() { - return Channel.#storage.get(this.id).rolePermissions; + return Channel.#get(this.id).rolePermissions; } /** * Whether this channel is marked as mature */ get mature() { - return Channel.#storage.get(this.id).nsfw; + return Channel.#get(this.id).nsfw; } /** * ID of the last message sent in this channel */ get lastMessageId() { - return Channel.#storage.get(this.id).lastMessageId; + return Channel.#get(this.id).lastMessageId; } /** @@ -341,4 +298,163 @@ export default (client: Client) => ...permission.map((x) => Permission[x]) ); } + + /** + * Fetch a channel's members. + * @requires `Group` + * @returns An array of the channel's members. + */ + async fetchMembers() { + const members = await client.api.get( + `/channels/${this.id as ""}/members` + ); + + return members.map((user) => client.User.new(user._id, user)); + } + + /** + * Edit a channel + * @param data Edit data + */ + async edit(data: DataEditChannel) { + await client.api.patch(`/channels/${this.id as ""}`, data); + } + + /** + * Delete a channel + * @param leave_silently Whether to not send a message on leave + * @param noRequest Whether to not send a request + * @requires `DM`, `Group`, `TextChannel`, `VoiceChannel` + */ + async delete(leave_silently?: boolean, noRequest?: boolean) { + if (!noRequest) + await client.api.delete(`/channels/${this.id as ""}`, { + leave_silently, + }); + + if (this.type === "DirectMessage") { + Channel.#set(this.id, "active", false); + return; + } + + if (this.type === "TextChannel" || this.type === "VoiceChannel") { + const server = this.server; + if (server) { + server.channelIds.delete(this.id); + } + } + + client.channels.delete(this.id); + } + + /** + * Add a user to a group + * @param user_id ID of the target user + */ + async addMember(user_id: string) { + return await client.api.put( + `/channels/${this.id as ""}/recipients/${user_id as ""}` + ); + } + + /** + * Remove a user from a group + * @param user_id ID of the target user + */ + async removeMember(user_id: string) { + return await client.api.delete( + `/channels/${this.id as ""}/recipients/${user_id as ""}` + ); + } + /** + * Send a message + * @param data Either the message as a string or message sending route data + * @returns The message + */ + async sendMessage( + data: string | DataMessageSend, + idempotencyKey: string = ulid() + ) { + const msg: DataMessageSend = + typeof data === "string" ? { content: data } : data; + + const message = await client.api.post( + `/channels/${this.id as ""}/messages`, + msg, + { + headers: { + "Idempotency-Key": idempotencyKey, + }, + } + ); + + return client.Message.new(message._id, message); + } + + /** + * Fetch a message by its ID + * @param messageId ID of the target message + * @returns The message + */ + async fetchMessage(messageId: string) { + const message = await client.api.get( + `/channels/${this.id as ""}/messages/${messageId as ""}` + ); + + return client.Message.new(message._id, message); + } + + /** + * Fetch multiple messages from a channel + * @param params Message fetching route data + * @returns The messages + */ + async fetchMessages( + params?: Omit< + (APIRoutes & { + method: "get"; + path: "/channels/{target}/messages"; + })["params"], + "include_users" + > + ) { + const messages = (await client.api.get( + `/channels/${this.id as ""}/messages`, + { ...params } + )) as Message[]; + + return messages.map((message) => + client.Message.new(message._id, message) + ); + } + + /** + * Fetch multiple messages from a channel including the users that sent them + * @param params Message fetching route data + * @returns Object including messages and users + */ + async fetchMessagesWithUsers( + params?: Omit< + (APIRoutes & { + method: "get"; + path: "/channels/{target}/messages"; + })["params"], + "include_users" + > + ) { + const data = (await client.api.get( + `/channels/${this.id as ""}/messages`, + { ...params, include_users: true } + )) as { messages: Message[]; users: User[]; members?: Member[] }; + + return { + messages: data.messages.map((message) => + client.Message.new(message._id, message) + ), + users: data.users.map((user) => client.User.new(user._id, user)), + members: data.members?.map((member) => + client.ServerMember.new(member._id, member) + ), + }; + } }; diff --git a/src/classes/Emoji.ts b/src/classes/Emoji.ts index 3e36459d..271312a0 100644 --- a/src/classes/Emoji.ts +++ b/src/classes/Emoji.ts @@ -1,93 +1,40 @@ -import { ReactiveMap } from "@solid-primitives/map"; +import { SetStoreFunction } from "solid-js/store"; + import type { Emoji as ApiEmoji } from "revolt-api"; import { decodeTime } from "ulid"; import { Client } from "../Client"; +import { StoreCollection } from "../collections/Collection"; import { HydratedEmoji } from "../hydration/emoji"; -import { ObjectStorage } from "../storage/ObjectStorage"; -export default (client: Client) => +export default ( + client: Client, + collection: StoreCollection +) => /** * Emoji Class */ class Emoji { - static #storage = new ObjectStorage(); + static #collection: StoreCollection< + InstanceType, + HydratedEmoji + >; + static #set: SetStoreFunction>; + static #get: (id: string) => HydratedEmoji; - // * Object Map Definition - static #objects = new ReactiveMap>(); - - /** - * Get an existing object - * @param id ID - * @returns Object - */ - static get(id: string): InstanceType | undefined { - return this.#objects.get(id); + static { + Emoji.#collection = collection as never; + Emoji.#set = collection.updateUnderlyingObject as never; + Emoji.#get = collection.getUnderlyingObject as never; } - /** - * Number of stored objects - * @returns Size - */ - static size() { - return this.#objects.size; - } - - /** - * Iterable of keys in the map - * @returns Iterable - */ - static keys() { - return this.#objects.keys(); - } - - /** - * Iterable of values in the map - * @returns Iterable - */ - static values() { - return this.#objects.values(); - } - - /** - * List of values in the map - * @returns List - */ - static toList() { - return [...this.#objects.values()]; - } - - /** - * Iterable of key, value pairs in the map - * @returns Iterable - */ - static entries() { - return this.#objects.entries(); - } - - /** - * Execute a provided function over each key, value pair in the map - * @param cb Callback for each pair - * @returns Iterable - */ - static forEach( - cb: ( - value: InstanceType, - key: string, - map: ReactiveMap> - ) => void - ) { - return this.#objects.forEach(cb); - } - // * End Object Map Definition - /** * Fetch emoji by ID * @param id ID * @returns Emoji */ static async fetch(id: string): Promise { - const emoji = Emoji.get(id); + const emoji = Emoji.#collection.get(id); if (emoji) return emoji; const data = await client.api.get(`/custom/emoji/${id as ""}`); @@ -102,8 +49,16 @@ export default (client: Client) => */ constructor(id: string, data?: ApiEmoji) { this.id = id; - Emoji.#storage.hydrate(id, "emoji", data); - Emoji.#objects.set(id, this); + Emoji.#collection.create(id, "emoji", this, data); + } + + /** + * Get or create + * @param id Id + * @param data Data + */ + static new(id: string, data?: ApiEmoji) { + return client.emojis.get(id) ?? new Emoji(id, data); } /** @@ -117,34 +72,34 @@ export default (client: Client) => * Information about the parent of this emoji */ get parent() { - return Emoji.#storage.get(this.id).parent; + return Emoji.#get(this.id).parent; } /** * Creator of the emoji */ get creator() { - return client.users.get(Emoji.#storage.get(this.id).creatorId); + return client.users.get(Emoji.#get(this.id).creatorId); } /** * Name */ get name() { - return Emoji.#storage.get(this.id).name; + return Emoji.#get(this.id).name; } /** * Whether the emoji is animated */ get animated() { - return Emoji.#storage.get(this.id).animated; + return Emoji.#get(this.id).animated; } /** * Whether the emoji is marked as mature */ get mature() { - return Emoji.#storage.get(this.id).nsfw; + return Emoji.#get(this.id).nsfw; } }; diff --git a/src/classes/Message.ts b/src/classes/Message.ts index 17d68230..011db373 100644 --- a/src/classes/Message.ts +++ b/src/classes/Message.ts @@ -1,86 +1,33 @@ -import { ReactiveMap } from "@solid-primitives/map"; +import { SetStoreFunction } from "solid-js/store"; + import type { Message as ApiMessage } from "revolt-api"; import { decodeTime } from "ulid"; import { Client } from "../Client"; +import { StoreCollection } from "../collections/Collection"; import { HydratedMessage } from "../hydration/message"; -import { ObjectStorage } from "../storage/ObjectStorage"; -export default (client: Client) => +export default ( + client: Client, + collection: StoreCollection +) => /** * Message Class */ class Message { - static #storage = new ObjectStorage(); + static #collection: StoreCollection< + InstanceType, + HydratedMessage + >; + static #set: SetStoreFunction>; + static #get: (id: string) => HydratedMessage; - // * Object Map Definition - static #objects = new ReactiveMap>(); - - /** - * Get an existing object - * @param id ID - * @returns Object - */ - static get(id: string): InstanceType | undefined { - return this.#objects.get(id); + static { + Message.#collection = collection as never; + Message.#set = collection.updateUnderlyingObject as never; + Message.#get = collection.getUnderlyingObject as never; } - /** - * Number of stored objects - * @returns Size - */ - static size() { - return this.#objects.size; - } - - /** - * Iterable of keys in the map - * @returns Iterable - */ - static keys() { - return this.#objects.keys(); - } - - /** - * Iterable of values in the map - * @returns Iterable - */ - static values() { - return this.#objects.values(); - } - - /** - * List of values in the map - * @returns List - */ - static toList() { - return [...this.#objects.values()]; - } - - /** - * Iterable of key, value pairs in the map - * @returns Iterable - */ - static entries() { - return this.#objects.entries(); - } - - /** - * Execute a provided function over each key, value pair in the map - * @param cb Callback for each pair - * @returns Iterable - */ - static forEach( - cb: ( - value: InstanceType, - key: string, - map: ReactiveMap> - ) => void - ) { - return this.#objects.forEach(cb); - } - // * End Object Map Definition - /** * Fetch message by ID * @param channelId Channel ID @@ -91,7 +38,7 @@ export default (client: Client) => channelId: string, messageId: string ): Promise { - const message = Message.get(messageId); + const message = Message.#collection.get(messageId); if (message) return message; const data = await client.api.get( @@ -108,8 +55,16 @@ export default (client: Client) => */ constructor(id: string, data?: ApiMessage) { this.id = id; - Message.#storage.hydrate(id, "message", data); - Message.#objects.set(id, this); + Message.#collection.create(id, "message", this, data); + } + + /** + * Get or create + * @param id Id + * @param data Data + */ + static new(id: string, data?: ApiMessage) { + return client.messages.get(id) ?? new Message(id, data); } /** @@ -123,104 +78,104 @@ export default (client: Client) => * Nonce value */ get nonce() { - return Message.#storage.get(this.id).nonce; + return Message.#get(this.id).nonce; } /** * Id of channel this message was sent in */ get channelId() { - return Message.#storage.get(this.id).channelId; + return Message.#get(this.id).channelId; } /** * Channel this message was sent in */ get channel() { - return client.channels.get(Message.#storage.get(this.id).channelId); + return client.channels.get(Message.#get(this.id).channelId); } /** * Id of user this message was sent by */ get authorId() { - return Message.#storage.get(this.id).authorId; + return Message.#get(this.id).authorId; } /** * User this message was sent by */ get author() { - return client.users.get(Message.#storage.get(this.id).authorId!); + return client.users.get(Message.#get(this.id).authorId!); } /** * Content */ get content() { - return Message.#storage.get(this.id).content; + return Message.#get(this.id).content; } /** * System message content */ get systemMessage() { - return Message.#storage.get(this.id).systemMessage; + return Message.#get(this.id).systemMessage; } /** * Attachments */ get attachments() { - return Message.#storage.get(this.id).attachments; + return Message.#get(this.id).attachments; } /** * Time at which this message was edited */ get editedAt() { - return Message.#storage.get(this.id).editedAt; + return Message.#get(this.id).editedAt; } /** * Embeds */ get embeds() { - return Message.#storage.get(this.id).embeds; + return Message.#get(this.id).embeds; } /** * IDs of users this message mentions */ get mentionIds() { - return Message.#storage.get(this.id).mentionIds; + return Message.#get(this.id).mentionIds; } /** * IDs of messages this message replies to */ get replyIds() { - return Message.#storage.get(this.id).replyIds; + return Message.#get(this.id).replyIds; } /** * Reactions */ get reactions() { - return Message.#storage.get(this.id).reactions; + return Message.#get(this.id).reactions; } /** * Interactions */ get interactions() { - return Message.#storage.get(this.id).interactions; + return Message.#get(this.id).interactions; } /** * Masquerade */ get masquerade() { - return Message.#storage.get(this.id).masquerade; + return Message.#get(this.id).masquerade; } }; diff --git a/src/classes/Server.ts b/src/classes/Server.ts index 4de4e948..77c63eda 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -1,95 +1,42 @@ -import { ReactiveMap } from "@solid-primitives/map"; +import { SetStoreFunction } from "solid-js/store"; + import type { Server as ApiServer, Category } from "revolt-api"; import { decodeTime } from "ulid"; import { Channel, Client } from "../Client"; +import { StoreCollection } from "../collections/Collection"; import { HydratedServer } from "../hydration/server"; import { bitwiseAndEq, calculatePermission } from "../permissions/calculator"; import { Permission } from "../permissions/definitions"; -import { ObjectStorage } from "../storage/ObjectStorage"; -export default (client: Client) => +export default ( + client: Client, + collection: StoreCollection +) => /** * Server Class */ class Server { - static #storage = new ObjectStorage(); + static #collection: StoreCollection< + InstanceType, + HydratedServer + >; + static #set: SetStoreFunction>; + static #get: (id: string) => HydratedServer; - // * Object Map Definition - static #objects = new ReactiveMap>(); - - /** - * Get an existing object - * @param id ID - * @returns Object - */ - static get(id: string): InstanceType | undefined { - return this.#objects.get(id); + static { + Server.#collection = collection as never; + Server.#set = collection.updateUnderlyingObject as never; + Server.#get = collection.getUnderlyingObject as never; } - /** - * Number of stored objects - * @returns Size - */ - static size() { - return this.#objects.size; - } - - /** - * Iterable of keys in the map - * @returns Iterable - */ - static keys() { - return this.#objects.keys(); - } - - /** - * Iterable of values in the map - * @returns Iterable - */ - static values() { - return this.#objects.values(); - } - - /** - * List of values in the map - * @returns List - */ - static toList() { - return [...this.#objects.values()]; - } - - /** - * Iterable of key, value pairs in the map - * @returns Iterable - */ - static entries() { - return this.#objects.entries(); - } - - /** - * Execute a provided function over each key, value pair in the map - * @param cb Callback for each pair - * @returns Iterable - */ - static forEach( - cb: ( - value: InstanceType, - key: string, - map: ReactiveMap> - ) => void - ) { - return this.#objects.forEach(cb); - } - // * End Object Map Definition - /** * Fetch server by ID * @param id ID * @returns Server */ static async fetch(id: string): Promise { - const server = Server.get(id); + const server = Server.#collection.get(id); if (server) return server; const data = await client.api.get(`/servers/${id as ""}`); @@ -99,13 +46,22 @@ export default (client: Client) => readonly id: string; /** - * Construct Server - * @param id Server Id + * Construct + * @param id Id + * @param data Data */ constructor(id: string, data?: ApiServer) { this.id = id; - Server.#storage.hydrate(id, "server", data); - Server.#objects.set(id, this); + Server.#collection.create(id, "server", this, data); + } + + /** + * Get or create + * @param id Id + * @param data Data + */ + static new(id: string, data?: ApiServer) { + return client.servers.get(id) ?? new Server(id, data); } /** @@ -119,49 +75,56 @@ export default (client: Client) => * Owner's user ID */ get ownerId() { - return Server.#storage.get(this.id).ownerId; + return Server.#get(this.id).ownerId; } /** * Owner */ get owner() { - return client.users.get(Server.#storage.get(this.id).ownerId); + return client.users.get(Server.#get(this.id).ownerId); } /** * Name */ get name() { - return Server.#storage.get(this.id).name; + return Server.#get(this.id).name; } /** * Description */ get description() { - return Server.#storage.get(this.id).description; + return Server.#get(this.id).description; } /** * Icon */ get icon() { - return Server.#storage.get(this.id).icon; + return Server.#get(this.id).icon; } /** * Banner */ get banner() { - return Server.#storage.get(this.id).banner; + return Server.#get(this.id).banner; + } + + /** + * Channel IDs + */ + get channelIds() { + return Server.#get(this.id).channelIds; } /** * Channels */ get channels() { - return [...Server.#storage.get(this.id).channelIds.values()] + return [...Server.#get(this.id).channelIds.values()] .map((id) => client.channels.get(id)!) .filter((x) => x); } @@ -170,56 +133,56 @@ export default (client: Client) => * Categories */ get categories() { - return Server.#storage.get(this.id).categories; + return Server.#get(this.id).categories; } /** * System message channels */ get systemMessages() { - return Server.#storage.get(this.id).systemMessages; + return Server.#get(this.id).systemMessages; } /** * Roles */ get roles() { - return Server.#storage.get(this.id).roles; + return Server.#get(this.id).roles; } /** * Default permissions */ get defaultPermissions() { - return Server.#storage.get(this.id).defaultPermissions; + return Server.#get(this.id).defaultPermissions; } /** * Server flags */ get flags() { - return Server.#storage.get(this.id).flags; + return Server.#get(this.id).flags; } /** * Whether analytics are enabled for this server */ get analytics() { - return Server.#storage.get(this.id).analytics; + return Server.#get(this.id).analytics; } /** * Whether this server is publicly discoverable */ get discoverable() { - return Server.#storage.get(this.id).discoverable; + return Server.#get(this.id).discoverable; } /** * Whether this server is marked as mature */ get mature() { - return Server.#storage.get(this.id).nsfw; + return Server.#get(this.id).nsfw; } /** @@ -336,7 +299,7 @@ export default (client: Client) => * Own member object for this server */ get member() { - return client.serverMembers.get({ + return client.serverMembers.getByKey({ server: this.id, user: client.user!.id, }); diff --git a/src/classes/ServerMember.ts b/src/classes/ServerMember.ts index f3c7a765..0df17b9b 100644 --- a/src/classes/ServerMember.ts +++ b/src/classes/ServerMember.ts @@ -1,11 +1,12 @@ -import { ReactiveMap } from "@solid-primitives/map"; +import { SetStoreFunction } from "solid-js/store"; + import type { Member as ApiMember, MemberCompositeKey } from "revolt-api"; import { Channel, Client, Server } from "../Client"; +import { StoreCollection } from "../collections/Collection"; import { HydratedServerMember } from "../hydration/serverMember"; import { bitwiseAndEq, calculatePermission } from "../permissions/calculator"; import { Permission } from "../permissions/definitions"; -import { ObjectStorage } from "../storage/ObjectStorage"; /** * Deterministic conversion of member composite key to string ID @@ -16,88 +17,27 @@ function key(key: MemberCompositeKey) { return key.server + key.user; } -export default (client: Client) => +export default ( + client: Client, + collection: StoreCollection +) => /** * Server Member Class */ class ServerMember { - static #storage = new ObjectStorage(); + static #collection: StoreCollection< + InstanceType, + HydratedServerMember + >; + static #set: SetStoreFunction>; + static #get: (id: string) => HydratedServerMember; - // * Object Map Definition - static #objects = new ReactiveMap>(); - - /** - * Deterministic conversion of member composite key to string ID - * @param key Key - * @returns String key - */ - static keyToString = key; - - /** - * Get an existing Member - * @param id Member ID - * @returns Member - */ - static get(id: MemberCompositeKey): ServerMember | undefined { - return ServerMember.#objects.get(key(id)); + static { + ServerMember.#collection = collection as never; + ServerMember.#set = collection.updateUnderlyingObject as never; + ServerMember.#get = collection.getUnderlyingObject as never; } - /** - * Number of stored objects - * @returns Size - */ - static size() { - return this.#objects.size; - } - - /** - * Iterable of keys in the map - * @returns Iterable - */ - static keys() { - return this.#objects.keys(); - } - - /** - * Iterable of values in the map - * @returns Iterable - */ - static values() { - return this.#objects.values(); - } - - /** - * List of values in the map - * @returns List - */ - static toList() { - return [...this.#objects.values()]; - } - - /** - * Iterable of key, value pairs in the map - * @returns Iterable - */ - static entries() { - return this.#objects.entries(); - } - - /** - * Execute a provided function over each key, value pair in the map - * @param cb Callback for each pair - * @returns Iterable - */ - static forEach( - cb: ( - value: InstanceType, - key: string, - map: ReactiveMap> - ) => void - ) { - return this.#objects.forEach(cb); - } - // * End Object Map Definition - /** * Fetch member by ID * @param id Member ID @@ -106,7 +46,7 @@ export default (client: Client) => static async fetch( id: MemberCompositeKey ): Promise { - const channel = ServerMember.get(id); + const channel = ServerMember.#collection.get(key(id)); if (channel) return channel; const data = await client.api.get( @@ -118,13 +58,22 @@ export default (client: Client) => readonly id: MemberCompositeKey; /** - * Construct Member - * @param id Member ID + * Construct + * @param id Id + * @param data Data */ constructor(id: MemberCompositeKey, data?: ApiMember) { - ServerMember.#storage.hydrate(key(id), "serverMember", data); - ServerMember.#objects.set(key(id), this); this.id = id; + ServerMember.#collection.create(key(id), "serverMember", this, data); + } + + /** + * Get or create + * @param id Id + * @param data Data + */ + static new(id: MemberCompositeKey, data?: ApiMember) { + return client.serverMembers.get(key(id)) ?? new ServerMember(id, data); } /** @@ -145,35 +94,35 @@ export default (client: Client) => * When this user joined the server */ get joinedAt() { - return ServerMember.#storage.get(key(this.id)).joinedAt; + return ServerMember.#get(key(this.id)).joinedAt; } /** * Nickname */ get nickname() { - return ServerMember.#storage.get(key(this.id)).nickname; + return ServerMember.#get(key(this.id)).nickname; } /** * Avatar */ get avatar() { - return ServerMember.#storage.get(key(this.id)).avatar; + return ServerMember.#get(key(this.id)).avatar; } /** * List of role IDs */ get roles() { - return ServerMember.#storage.get(key(this.id)).roles; + return ServerMember.#get(key(this.id)).roles; } /** * Time at which timeout expires */ get timeout() { - return ServerMember.#storage.get(key(this.id)).timeout; + return ServerMember.#get(key(this.id)).timeout; } /** diff --git a/src/classes/User.ts b/src/classes/User.ts index 4f2dcd34..38456569 100644 --- a/src/classes/User.ts +++ b/src/classes/User.ts @@ -1,94 +1,41 @@ -import { ReactiveMap } from "@solid-primitives/map"; +import { SetStoreFunction } from "solid-js/store"; + import type { User as ApiUser } from "revolt-api"; import { decodeTime } from "ulid"; import { Client, FileArgs } from "../Client"; +import { StoreCollection } from "../collections/Collection"; import { HydratedUser } from "../hydration/user"; import { U32_MAX, UserPermission } from "../permissions/definitions"; -import { ObjectStorage } from "../storage/ObjectStorage"; -export default (client: Client) => +export default ( + client: Client, + collection: StoreCollection +) => /** * User Class */ class User { - static #storage = new ObjectStorage(); + static #collection: StoreCollection< + InstanceType, + HydratedUser + >; + static #set: SetStoreFunction>; + static #get: (id: string) => HydratedUser; - // * Object Map Definition - static #objects = new ReactiveMap>(); - - /** - * Get an existing object - * @param id ID - * @returns Object - */ - static get(id: string): InstanceType | undefined { - return this.#objects.get(id); + static { + User.#collection = collection as never; + User.#set = collection.updateUnderlyingObject as never; + User.#get = collection.getUnderlyingObject as never; } - /** - * Number of stored objects - * @returns Size - */ - static size() { - return this.#objects.size; - } - - /** - * Iterable of keys in the map - * @returns Iterable - */ - static keys() { - return this.#objects.keys(); - } - - /** - * Iterable of values in the map - * @returns Iterable - */ - static values() { - return this.#objects.values(); - } - - /** - * List of values in the map - * @returns List - */ - static toList() { - return [...this.#objects.values()]; - } - - /** - * Iterable of key, value pairs in the map - * @returns Iterable - */ - static entries() { - return this.#objects.entries(); - } - - /** - * Execute a provided function over each key, value pair in the map - * @param cb Callback for each pair - * @returns Iterable - */ - static forEach( - cb: ( - value: InstanceType, - key: string, - map: ReactiveMap> - ) => void - ) { - return this.#objects.forEach(cb); - } - // * End Object Map Definition - /** * Fetch user by ID * @param id ID * @returns User */ static async fetch(id: string): Promise { - const user = this.get(id); + const user = this.#collection.get(id); if (user) return user; const data = await client.api.get(`/users/${id as ""}`); @@ -98,13 +45,22 @@ export default (client: Client) => readonly id: string; /** - * Construct User - * @param id User Id + * Construct + * @param id Id + * @param data Data */ constructor(id: string, data?: ApiUser) { this.id = id; - User.#storage.hydrate(id, "user", data); - User.#objects.set(id, this); + User.#collection.create(id, "user", this, data); + } + + /** + * Get or create + * @param id Id + * @param data Data + */ + static new(id: string, data?: ApiUser) { + return client.users.get(id) ?? new User(id, data); } /** @@ -118,63 +74,63 @@ export default (client: Client) => * Username */ get username() { - return User.#storage.get(this.id).username; + return User.#get(this.id).username; } /** * Avatar */ get avatar() { - return User.#storage.get(this.id).avatar; + return User.#get(this.id).avatar; } /** * Badges */ get badges() { - return User.#storage.get(this.id).badges; + return User.#get(this.id).badges; } /** * User Status */ get status() { - return User.#storage.get(this.id).status; + return User.#get(this.id).status; } /** * Relationship with user */ get relationship() { - return User.#storage.get(this.id).relationship; + return User.#get(this.id).relationship; } /** * Whether the user is online */ get online() { - return User.#storage.get(this.id).online; + return User.#get(this.id).online; } /** * Whether the user is privileged */ get privileged() { - return User.#storage.get(this.id).privileged; + return User.#get(this.id).privileged; } /** * Flags */ get flags() { - return User.#storage.get(this.id).flags; + return User.#get(this.id).flags; } /** * Bot information */ get bot() { - return User.#storage.get(this.id).bot; + return User.#get(this.id).bot; } /** diff --git a/src/collections/Collection.ts b/src/collections/Collection.ts new file mode 100644 index 00000000..cb5ad716 --- /dev/null +++ b/src/collections/Collection.ts @@ -0,0 +1,170 @@ +import { SetStoreFunction } from "solid-js/store"; + +import { ReactiveMap } from "@solid-primitives/map"; + +import { Hydrators, hydrate } from "../hydration"; +import { ObjectStorage } from "../storage/ObjectStorage"; + +/** + * Abstract Collection type + */ +export abstract class Collection { + /** + * Get an existing object + * @param id Id + * @returns Object + */ + abstract get(id: string): T | undefined; + + /** + * Check whether an id exists in the Collection + * @param id Id + * @returns Whether it exists + */ + abstract has(id: string): boolean; + + /** + * Delete an object + * @param id Id + */ + abstract delete(id: string): void; + + /** + * Number of stored objects + * @returns Size + */ + abstract size(): number; + + /** + * Iterable of keys in the map + * @returns Iterable + */ + abstract keys(): IterableIterator; + + /** + * Iterable of values in the map + * @returns Iterable + */ + abstract values(): IterableIterator; + + /** + * Iterable of key, value pairs in the map + * @returns Iterable + */ + abstract entries(): IterableIterator<[string, T]>; + + /** + * List of values in the map + * @returns List + */ + toList() { + return [...this.values()]; + } + + /** + * Execute a provided function over each key, value pair in the map + * @param cb Callback for each pair + */ + abstract forEach( + cb: (value: T, key: string, map: ReactiveMap) => void + ): void; +} + +/** + * Collection backed by a Solid.js Store + */ +export abstract class StoreCollection extends Collection { + #storage = new ObjectStorage(); + #objects = new ReactiveMap(); + readonly getUnderlyingObject: (id: string) => V; + readonly updateUnderlyingObject: SetStoreFunction>; + + /** + * Construct store backed collection + */ + constructor() { + super(); + this.getUnderlyingObject = this.#storage.get; + this.updateUnderlyingObject = this.#storage.set; + } + + /** + * Get an existing object + * @param id Id + * @returns Object + */ + get(id: string): T | undefined { + return this.#objects.get(id); + } + + /** + * Check whether an id exists in the Collection + * @param id Id + * @returns Whether it exists + */ + has(id: string) { + return this.#objects.has(id); + } + + /** + * Delete an object + * @param id Id + */ + delete(id: string): void { + this.#objects.delete(id); + this.updateUnderlyingObject(id, undefined as never); + } + + /** + * Create a new instance of an object + * @param id Id + * @param type Type + * @param instance Instance + * @param data Data + */ + create(id: string, type: keyof Hydrators, instance: T, data?: unknown) { + this.#storage.hydrate(id, type, data); + this.#objects.set(id, instance); + } + + /** + * Number of stored objects + * @returns Size + */ + size() { + return this.#objects.size; + } + + /** + * Iterable of keys in the map + * @returns Iterable + */ + keys() { + return this.#objects.keys(); + } + + /** + * Iterable of values in the map + * @returns Iterable + */ + values() { + return this.#objects.values(); + } + + /** + * Iterable of key, value pairs in the map + * @returns Iterable + */ + entries() { + return this.#objects.entries(); + } + + /** + * Execute a provided function over each key, value pair in the map + * @param cb Callback for each pair + * @returns Iterable + */ + forEach(cb: (value: T, key: string, map: ReactiveMap) => void) { + return this.#objects.forEach(cb); + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts new file mode 100644 index 00000000..c9fbc7c8 --- /dev/null +++ b/src/collections/index.ts @@ -0,0 +1,31 @@ +import { MemberCompositeKey } from "revolt-api"; + +import { Channel, Emoji, Message, Server, ServerMember, User } from ".."; +import { HydratedChannel } from "../hydration/channel"; +import { HydratedEmoji } from "../hydration/emoji"; +import { HydratedMessage } from "../hydration/message"; +import { HydratedServer } from "../hydration/server"; +import { HydratedServerMember } from "../hydration/serverMember"; +import { HydratedUser } from "../hydration/user"; + +import { StoreCollection } from "./Collection"; + +export class ChannelCollection extends StoreCollection< + Channel, + HydratedChannel +> {} +export class EmojiCollection extends StoreCollection {} +export class MessageCollection extends StoreCollection< + Message, + HydratedMessage +> {} +export class ServerCollection extends StoreCollection {} +export class UserCollection extends StoreCollection {} +export class ServerMemberCollection extends StoreCollection< + ServerMember, + HydratedServerMember +> { + getByKey(id: MemberCompositeKey) { + return super.get(id.server + id.user); + } +} diff --git a/src/index.ts b/src/index.ts index 49cbdf3a..c55a94ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export type { ServerMember, User, Session, + Message, } from "./Client"; export * from "./lib/regex"; diff --git a/src/permissions/calculator.ts b/src/permissions/calculator.ts index aca6e6ac..b972b30f 100644 --- a/src/permissions/calculator.ts +++ b/src/permissions/calculator.ts @@ -40,14 +40,14 @@ export function calculatePermission( return Permission.GrantAllSafe; } - if (target instanceof client.servers) { + if (target instanceof client.Server) { // 1. Check if owner. if (target.owner === user?.id) { return Permission.GrantAllSafe; } else { // 2. Get ServerMember. const member = options?.member ?? - client.serverMembers.get({ + client.serverMembers.getByKey({ user: user!.id, server: target.id, }) ?? { roles: null, timeout: null }; @@ -113,7 +113,7 @@ export function calculatePermission( } else { // 4. Get ServerMember. const member = options?.member ?? - client.serverMembers.get({ + client.serverMembers.getByKey({ user: user!.id, server: server.id, }) ?? { roles: null, timeout: null }; diff --git a/src/storage/ObjectStorage.ts b/src/storage/ObjectStorage.ts index fee6b373..0e116e19 100644 --- a/src/storage/ObjectStorage.ts +++ b/src/storage/ObjectStorage.ts @@ -16,6 +16,7 @@ export class ObjectStorage { const [store, setStore] = createStore({}); this.store = store as never; this.set = setStore; + this.get = this.get.bind(this); } /**