@solid-data-modules/chats-ldo
Version:
A library to mangage chats in Solid Pods built with LDO
276 lines • 11.2 kB
JavaScript
import { scheduleNewDayTrigger } from "./util/scheduleNewDayTrigger.js";
import { ChatMessageListShapeShapeType, ChatMessageShapeShapeType, ChatShapeShapeType } from "./.ldo/longChat.shapeTypes.js";
import { getResource, throwIfErr, throwIfErrOrAbsent } from "./util/resultHelpers.js";
import { v4 } from "uuid";
export class Chat {
constructor(containerUri, dataset) {
this.mostRecentMessageDate = new Date(0).toISOString();
this.containerResource = dataset.getResource(containerUri);
this.chatResource = this.containerResource.child("index.ttl");
this.dataset = dataset;
this.clearNewDayTrigger = scheduleNewDayTrigger(this.onNewDay.bind(this));
}
/**
* ===========================================================================
* HELPERS
* ===========================================================================
*/
async onNewDay() {
// Unsubscribe from the previous day's resource
await this.endNotificationSubscription();
// Create a new document for today
this.todaysMessageResource = await this.getOrCreateTodaysMessageResource();
// Subscribe to it if a subscription exists
await this.startNotificationSubscription();
}
/**
* Gets the resource for today's messages. If it doesn't exist, create it.
*/
async getOrCreateTodaysMessageResource() {
// Get Todays date
const today = new Date();
const year = today.getUTCFullYear();
const month = today.getUTCMonth() + 1;
const day = today.getUTCDate();
// Year
const yearResource = getResource(await this.containerResource.createChildIfAbsent(`${year}/`));
// Month
const monthResource = getResource(await yearResource.createChildIfAbsent(`${month}/`));
// Day
const dayResource = getResource(await monthResource.createChildIfAbsent(`${day}/`));
// Index
const indexResource = getResource(await dayResource.createChildIfAbsent(`index.ttl`));
return indexResource;
}
/**
* Ensures today's message resource is identified
*/
async ensureTodaysMessageResource() {
if (this.todaysMessageResource)
return this.todaysMessageResource;
this.todaysMessageResource = await this.getOrCreateTodaysMessageResource();
// Set the most recent message time
const [mostRecentMessage] = this.getMessagesForMessageResource(this.todaysMessageResource);
if (mostRecentMessage)
this.mostRecentMessageDate = mostRecentMessage.created2;
return this.todaysMessageResource;
}
/**
* Given a message resource, find the message resource that is chronologically
* previous to this one. Return undefined if there is none.
* @param currentResource
*/
async getPreviousMessageResource(currentResource) {
const getPreviousContainer = (list, target) => {
const sorted = [...list]
.filter((item) => item.type === "SolidContainer")
.sort((a, b) => a.uri.localeCompare(b.uri));
const sliced = sorted.slice(0, sorted.findIndex(i => i.uri === target.uri));
return sliced.pop();
};
const getMostRecentContainer = (list) => [...list]
.filter((item) => item.type === "SolidContainer")
.sort((a, b) => a.uri.localeCompare(b.uri))
.pop();
const getIndexIfExists = async (container) => {
const indexResource = container.child("index.ttl");
throwIfErrOrAbsent(await indexResource.readIfUnfetched());
return indexResource;
};
const drillDownMostRecent = async (container, levels) => {
if (levels === 0)
return getIndexIfExists(container);
throwIfErrOrAbsent(await container.readIfUnfetched());
const mostRecent = getMostRecentContainer(container.children());
if (!mostRecent)
return undefined;
return drillDownMostRecent(mostRecent, levels - 1);
};
let container = await currentResource.getParentContainer(); // Day
const levelNames = ["Month", "Year", "Eternity"]; // Logical structure
for (let depth = 0; depth < levelNames.length; depth++) {
const parent = await container.getParentContainer();
throwIfErrOrAbsent(await parent.readIfUnfetched());
const previous = getPreviousContainer(parent.children(), container);
if (previous) {
throwIfErrOrAbsent(await previous.readIfUnfetched());
return drillDownMostRecent(previous, depth); // Drill into [day, month, year]
}
container = parent;
}
return undefined;
}
/**
* Begins notification subscription with today's message resource
*/
async startNotificationSubscription() {
await this.ensureTodaysMessageResource();
if (this.subscriptionCallback) {
console.log(this.todaysMessageResource.isPresent());
this.internalCallback = () => {
console.log("Callback Called:", this.todaysMessageResource);
const messages = this.getMessagesForMessageResource(this.todaysMessageResource);
const newMessages = messages.filter((message) => {
return message.created2 > this.mostRecentMessageDate;
});
if (newMessages.length > 0) {
this.mostRecentMessageDate = newMessages[0].created2;
this.subscriptionCallback?.(newMessages);
}
};
this.subscriptionId = await this.todaysMessageResource
.subscribeToNotifications({
onNotification: this.internalCallback.bind(this),
});
}
}
/**
* Ends notification subscription with today's message resource
*/
async endNotificationSubscription() {
await this.ensureTodaysMessageResource();
if (this.subscriptionId) {
await this.todaysMessageResource
.unsubscribeFromNotifications(this.subscriptionId);
this.subscriptionId = undefined;
}
}
/**
* Returns the messages in a resource sorted in order of most recent to least
* recent
* @param resource
*/
getMessagesForMessageResource(resource) {
const chatMessageList = this.dataset
.usingType(ChatMessageShapeShapeType)
.matchObject(`${this.chatResource.uri}#this`, "http://www.w3.org/2005/01/wf/flow#message", `${resource.uri}`);
return chatMessageList.toArray()
.sort((a, b) => b.created2.localeCompare(a.created2)) ?? [];
}
/**
* ===========================================================================
* PUBLIC METHODS
* ===========================================================================
*/
/**
* Creates a chat. Throws an error if it already esists
* @param chatInfo
*/
async createChat(chatInfo) {
const createResult = throwIfErr(await this.chatResource.createIfAbsent());
if (createResult.type !== "createSuccess")
throw new Error("Could not create chat. Chat already exists");
await this.setChatInfo(chatInfo);
}
/**
* Gets a chat's info
* @returns
*/
async getChatInfo() {
const result = throwIfErrOrAbsent(await this.chatResource.readIfUnfetched());
return this.dataset.usingType(ChatShapeShapeType)
.fromSubject(`${this.chatResource.uri}#this`);
}
async setChatInfo(chatInfo) {
await this.chatResource.readIfUnfetched();
const chatInfoTransaction = this.dataset
.startTransaction();
const cChatInfo = chatInfoTransaction
.usingType(ChatShapeShapeType)
.write(this.chatResource.uri)
.fromSubject(`${this.chatResource.uri}#this`);
Object.assign(cChatInfo, chatInfo);
throwIfErr(await chatInfoTransaction.commitToRemote());
}
/**
* Deletes this chat
*/
async deleteChat() {
await this.endNotificationSubscription();
throwIfErr(await this.containerResource.delete());
}
/**
* Deletes the given message
* @param messageId
*/
async removeMessage(messageId) {
const messageTransaction = this.dataset
.startTransaction();
const cMessage = messageTransaction
.usingType(ChatMessageShapeShapeType)
.fromSubject(messageId);
delete cMessage["@id"];
throwIfErr(await messageTransaction.commitToRemote());
}
/**
* Sends a message to the chat
* @param content
* @param makerWebId
*/
async sendMessage(content, makerWebId) {
await this.ensureTodaysMessageResource();
const messageTransaction = this.dataset
.startTransaction();
const cChatMessageList = messageTransaction
.usingType(ChatMessageListShapeShapeType)
.write(this.todaysMessageResource.uri)
.fromSubject(`${this.chatResource.uri}#this`);
const messageDate = new Date().toISOString();
cChatMessageList.message?.add({
"@id": `${this.todaysMessageResource.uri}#${v4()}`,
content,
maker: { "@id": makerWebId },
created2: messageDate,
});
const result = throwIfErr(await messageTransaction.commitToRemote());
this.mostRecentMessageDate = messageDate;
}
/**
* Returns an iterator to fetch paginated messages.
* @returns
*/
getMessageIterator() {
const self = this;
async function* messageGenerator() {
await self.ensureTodaysMessageResource();
let currentResource = self.todaysMessageResource;
while (currentResource) {
const messages = self.getMessagesForMessageResource(currentResource);
yield messages;
currentResource = await self.getPreviousMessageResource(currentResource);
}
}
return messageGenerator();
}
/**
* Subscribe to new incoming messages for this chat
* @param callback
* @returns
*/
async subscribeToMessages(callback) {
// If Subscription Callback already exists, simply update it and continue
// the subscription
if (this.subscriptionCallback) {
this.subscriptionCallback = callback;
return;
}
this.subscriptionCallback = callback;
// Begin subscription
await this.startNotificationSubscription();
}
/**
* Unsubscribe from notifications
*/
async unsubscribeFromMessages() {
this.subscriptionCallback = undefined;
await this.endNotificationSubscription();
}
/**
* Call this before destroying the class to clean up listeners.
*/
async destroy() {
this.clearNewDayTrigger?.();
await this.endNotificationSubscription();
}
}
//# sourceMappingURL=Chat.js.map