diff --git a/src/app/content/components/MessageItem.tsx b/src/app/content/components/MessageItem.tsx index bb0b0a7..3b11d4d 100644 --- a/src/app/content/components/MessageItem.tsx +++ b/src/app/content/components/MessageItem.tsx @@ -58,7 +58,7 @@ const MessageItem: FC = (props) => {
{props.data.username}
- +
diff --git a/src/app/content/index.tsx b/src/app/content/index.tsx index 960b54a..20a2d77 100644 --- a/src/app/content/index.tsx +++ b/src/app/content/index.tsx @@ -56,13 +56,13 @@ export default defineContentScript({ container.append(app) const root = createRoot(app) root.render( - - - - - - - + // + + + + + + // ) return root }, diff --git a/src/app/content/views/Footer/index.tsx b/src/app/content/views/Footer/index.tsx index ad0f6bb..f67182d 100644 --- a/src/app/content/views/Footer/index.tsx +++ b/src/app/content/views/Footer/index.tsx @@ -5,7 +5,7 @@ import MessageInput from '../../components/MessageInput' import EmojiButton from '../../components/EmojiButton' import { Button } from '@/components/ui/Button' import MessageInputDomain from '@/domain/MessageInput' -import { MESSAGE_MAX_LENGTH } from '@/constants/config' +import { MESSAGE_MAX_LENGTH, WEB_RTC_MAX_MESSAGE_SIZE } from '@/constants/config' import RoomDomain from '@/domain/Room' import useCursorPosition from '@/hooks/useCursorPosition' import useShareRef from '@/hooks/useShareRef' @@ -15,7 +15,7 @@ import useTriggerAway from '@/hooks/useTriggerAway' import { ScrollArea } from '@/components/ui/ScrollArea' import { Virtuoso, VirtuosoHandle } from 'react-virtuoso' import UserInfoDomain from '@/domain/UserInfo' -import { blobToBase64, cn, compressImage, getRootNode, getTextSimilarity } from '@/utils' +import { blobToBase64, cn, compressImage, getRootNode, getTextByteSize, getTextSimilarity } from '@/utils' import { Avatar, AvatarFallback } from '@/components/ui/Avatar' import { AvatarImage } from '@radix-ui/react-avatar' import ToastDomain from '@/domain/Toast' @@ -136,6 +136,13 @@ const Footer: FC = () => { }) .filter(Boolean) + const newMessage = { body: transformedMessage, atUsers } + const byteSize = getTextByteSize(JSON.stringify(newMessage)) + + if (byteSize > WEB_RTC_MAX_MESSAGE_SIZE) { + return send(toastDomain.command.WarningCommand('Message size cannot exceed 256KiB.')) + } + send(roomDomain.command.SendTextMessageCommand({ body: transformedMessage, atUsers })) send(messageInputDomain.command.ClearCommand()) } diff --git a/src/app/content/views/Main/index.tsx b/src/app/content/views/Main/index.tsx index 36d3b44..bfb6ada 100644 --- a/src/app/content/views/Main/index.tsx +++ b/src/app/content/views/Main/index.tsx @@ -15,16 +15,18 @@ const Main: FC = () => { const userInfoDomain = useRemeshDomain(UserInfoDomain()) const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery()) const _messageList = useRemeshQuery(messageListDomain.query.ListQuery()) - const messageList = _messageList.map((message) => { - if (message.type === MessageType.Normal) { - return { - ...message, - like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id), - hate: message.hateUsers.some((hateUser) => hateUser.userId === userInfo?.id) + const messageList = _messageList + .map((message) => { + if (message.type === MessageType.Normal) { + return { + ...message, + like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id), + hate: message.hateUsers.some((hateUser) => hateUser.userId === userInfo?.id) + } } - } - return message - }) + return message + }) + .toSorted((a, b) => a.worldTime - b.worldTime) const handleLikeChange = (messageId: string) => { send(roomDomain.command.SendLikeMessageCommand(messageId)) diff --git a/src/app/content/views/Setup/index.tsx b/src/app/content/views/Setup/index.tsx index a018be5..45a4c43 100644 --- a/src/app/content/views/Setup/index.tsx +++ b/src/app/content/views/Setup/index.tsx @@ -33,8 +33,6 @@ const mockTextList = [ `![ExampleImage](${ExampleImage})` ] -let printTextList = [...mockTextList] - const generateUserInfo = async (): Promise => { return { id: nanoid(), @@ -52,8 +50,10 @@ const generateMessage = async (userInfo: UserInfo): Promise => { const { name: username, avatar: userAvatar, id: userId } = userInfo return { id: nanoid(), - body: printTextList.shift()!, - date: Date.now(), + body: mockTextList.shift()!, + sendTime: Date.now(), + receiveTime: Date.now(), + worldTime: Date.now(), type: MessageType.Normal, userId, username, @@ -87,19 +87,16 @@ const Setup: FC = () => { } useEffect(() => { - printTextList.length === 0 && (printTextList = [...mockTextList]) const timer = new Timer( async () => { await createMessage(await refreshUserInfo()) }, - { delay: 2000, immediate: true, limit: printTextList.length } + { delay: 2000, immediate: true, limit: mockTextList.length } ) - timer.on('stop', () => { - printTextList.length === 0 && send(messageListDomain.command.ClearListCommand()) - }) timer.start() return () => { timer.stop() + send(messageListDomain.command.ClearListCommand()) } }, []) diff --git a/src/constants/config.ts b/src/constants/config.ts index 78c8786..d509438 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -199,3 +199,11 @@ export const APP_STATUS_STORAGE_KEY = 'WEB_CHAT_APP_STATUS' as const * 8kb * (1 - 0.33) = 5488 bytes */ export const MAX_AVATAR_SIZE = 5120 as const + +export const SYNC_HISTORY_MAX_DAYS = 30 as const + +/** + * https://lgrahl.de/articles/demystifying-webrtc-dc-size-limit.html + * Message max size is 256KiB; if the message is too large, it will cause the connection to drop. + */ +export const WEB_RTC_MAX_MESSAGE_SIZE = 262144 as const diff --git a/src/domain/MessageList.ts b/src/domain/MessageList.ts index c715784..ef5309f 100644 --- a/src/domain/MessageList.ts +++ b/src/domain/MessageList.ts @@ -24,7 +24,9 @@ export interface NormalMessage extends MessageUser { type: MessageType.Normal id: string body: string - date: number + sendTime: number + receiveTime: number + worldTime: number likeUsers: MessageUser[] hateUsers: MessageUser[] atUsers: AtUser[] @@ -34,7 +36,9 @@ export interface PromptMessage extends MessageUser { type: MessageType.Prompt id: string body: string - date: number + sendTime: number + receiveTime: number + worldTime: number } export type Message = NormalMessage | PromptMessage @@ -120,6 +124,38 @@ const MessageListDomain = Remesh.domain({ } }) + const UpsertItemCommand = domain.command({ + name: 'MessageList.UpsertItemCommand', + impl: (_, message: Message) => { + return [ + MessageListModule.command.UpsertItemCommand(message), + UpsertItemEvent(message), + ChangeListEvent(), + SyncToStorageEvent() + ] + } + }) + + const UpsertItemEvent = domain.event({ + name: 'MessageList.UpsertItemEvent' + }) + + const ResetListCommand = domain.command({ + name: 'MessageList.ResetListCommand', + impl: (_, messages: Message[]) => { + return [ + MessageListModule.command.SetListCommand(messages), + ResetListEvent(messages), + ChangeListEvent(), + SyncToStorageEvent() + ] + } + }) + + const ResetListEvent = domain.event({ + name: 'MessageList.ResetListEvent' + }) + const ClearListEvent = domain.event({ name: 'MessageList.ClearListEvent' }) @@ -164,14 +200,18 @@ const MessageListDomain = Remesh.domain({ CreateItemCommand, UpdateItemCommand, DeleteItemCommand, - ClearListCommand + UpsertItemCommand, + ClearListCommand, + ResetListCommand }, event: { ChangeListEvent, CreateItemEvent, UpdateItemEvent, DeleteItemEvent, + UpsertItemEvent, ClearListEvent, + ResetListEvent, SyncToStateEvent, SyncToStorageEvent } diff --git a/src/domain/Room.ts b/src/domain/Room.ts index 0dfd44b..eb67834 100644 --- a/src/domain/Room.ts +++ b/src/domain/Room.ts @@ -4,33 +4,51 @@ import { AtUser, NormalMessage, type MessageUser } from './MessageList' import { PeerRoomExtern } from '@/domain/externs/PeerRoom' import MessageListDomain, { MessageType } from '@/domain/MessageList' import UserInfoDomain from '@/domain/UserInfo' -import { desert, upsert } from '@/utils' +import { desert, getTextByteSize, upsert } from '@/utils' import { nanoid } from 'nanoid' import StatusModule from '@/domain/modules/Status' +import { ToastExtern } from './externs/Toast' +import { SYNC_HISTORY_MAX_DAYS, WEB_RTC_MAX_MESSAGE_SIZE } from '@/constants/config' export { MessageType } export enum SendType { - Like = 'like', - Hate = 'hate', - Text = 'text', - Join = 'join' + Like = 'Like', + Hate = 'Hate', + Text = 'Text', + SyncUser = 'SyncUser', + SyncHistory = 'SyncHistory' } export interface SyncUserMessage extends MessageUser { - type: SendType.Join + type: SendType.SyncUser id: string peerId: string joinTime: number + sendTime: number + worldTime: number + lastMessageTime: number +} + +export interface SyncHistoryMessage extends MessageUser { + type: SendType.SyncHistory + sendTime: number + worldTime: number + id: string + messages: NormalMessage[] } export interface LikeMessage extends MessageUser { type: SendType.Like + sendTime: number + worldTime: number id: string } export interface HateMessage extends MessageUser { type: SendType.Hate + sendTime: number + worldTime: number id: string } @@ -38,10 +56,12 @@ export interface TextMessage extends MessageUser { type: SendType.Text id: string body: string + sendTime: number + worldTime: number atUsers: AtUser[] } -export type RoomMessage = SyncUserMessage | LikeMessage | HateMessage | TextMessage +export type RoomMessage = SyncUserMessage | SyncHistoryMessage | LikeMessage | HateMessage | TextMessage export type RoomUser = MessageUser & { peerId: string; joinTime: number } @@ -50,6 +70,7 @@ const RoomDomain = Remesh.domain({ impl: (domain) => { const messageListDomain = domain.getDomain(MessageListDomain()) const userInfoDomain = domain.getDomain(UserInfoDomain()) + const toast = domain.getExtern(ToastExtern) const peerRoom = domain.getExtern(PeerRoomExtern) const PeerIdState = domain.state({ @@ -80,6 +101,24 @@ const RoomDomain = Remesh.domain({ } }) + const SelfUserQuery = domain.query({ + name: 'Room.SelfUserQuery', + impl: ({ get }) => { + return get(UserListQuery()).find((user) => user.peerId === get(PeerIdQuery()))! + } + }) + + const LastMessageTimeQuery = domain.query({ + name: 'Room.LastMessageTimeQuery', + impl: ({ get }) => { + return ( + get(messageListDomain.query.ListQuery()) + .filter((message) => message.type === MessageType.Normal) + .toSorted((a, b) => b.worldTime - a.worldTime)[0]?.worldTime ?? new Date(1970, 1, 1).getTime() + ) + } + }) + const JoinIsFinishedQuery = JoinStatusModule.query.IsFinishedQuery const JoinRoomCommand = domain.command({ @@ -100,7 +139,9 @@ const RoomDomain = Remesh.domain({ userAvatar, body: `"${username}" joined the chat`, type: MessageType.Prompt, - date: Date.now() + sendTime: Date.now(), + receiveTime: Date.now(), + worldTime: Date.now() }), JoinStatusModule.command.SetFinishedCommand(), JoinRoomEvent(peerRoom.roomId) @@ -121,7 +162,9 @@ const RoomDomain = Remesh.domain({ userAvatar, body: `"${username}" left the chat`, type: MessageType.Prompt, - date: Date.now() + sendTime: Date.now(), + receiveTime: Date.now(), + worldTime: Date.now() }), UpdateUserListCommand({ type: 'delete', @@ -136,22 +179,22 @@ const RoomDomain = Remesh.domain({ const SendTextMessageCommand = domain.command({ name: 'Room.SendTextMessageCommand', impl: ({ get }, message: string | { body: string; atUsers: AtUser[] }) => { - const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + const self = get(SelfUserQuery()) const textMessage: TextMessage = { + ...self, id: nanoid(), type: SendType.Text, + sendTime: Date.now(), + worldTime: Date.now(), body: typeof message === 'string' ? message : message.body, - userId, - username, - userAvatar, atUsers: typeof message === 'string' ? [] : message.atUsers } const listMessage: NormalMessage = { ...textMessage, type: MessageType.Normal, - date: Date.now(), + receiveTime: Date.now(), likeUsers: [], hateUsers: [], atUsers: typeof message === 'string' ? [] : message.atUsers @@ -165,14 +208,14 @@ const RoomDomain = Remesh.domain({ const SendLikeMessageCommand = domain.command({ name: 'Room.SendLikeMessageCommand', impl: ({ get }, messageId: string) => { - const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + const self = get(SelfUserQuery()) const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage const likeMessage: LikeMessage = { + ...self, id: messageId, - userId, - username, - userAvatar, + sendTime: Date.now(), + worldTime: Date.now(), type: SendType.Like } const listMessage: NormalMessage = { @@ -187,14 +230,14 @@ const RoomDomain = Remesh.domain({ const SendHateMessageCommand = domain.command({ name: 'Room.SendHateMessageCommand', impl: ({ get }, messageId: string) => { - const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + const self = get(SelfUserQuery()) const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage const hateMessage: HateMessage = { + ...self, id: messageId, - userId, - username, - userAvatar, + sendTime: Date.now(), + worldTime: Date.now(), type: SendType.Hate } const listMessage: NormalMessage = { @@ -206,19 +249,92 @@ const RoomDomain = Remesh.domain({ } }) - const SendJoinMessageCommand = domain.command({ - name: 'Room.SendJoinMessageCommand', - impl: ({ get }, targetPeerId: string) => { - const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)! + const SendSyncUserMessageCommand = domain.command({ + name: 'Room.SendSyncUserMessageCommand', + impl: ({ get }, peerId: string) => { + const self = get(SelfUserQuery()) + const lastMessageTime = get(LastMessageTimeQuery()) const syncUserMessage: SyncUserMessage = { ...self, id: nanoid(), - type: SendType.Join + sendTime: Date.now(), + worldTime: Date.now(), + lastMessageTime, + type: SendType.SyncUser } - peerRoom.sendMessage(syncUserMessage, targetPeerId) - return [SendJoinMessageEvent(syncUserMessage)] + peerRoom.sendMessage(syncUserMessage, peerId) + return [SendSyncUserMessageEvent(syncUserMessage)] + } + }) + + /** + * The maximum sync message is the historical records within 30 days, using the last message as the basis for judgment. + * The number of synced messages may not be all messages within 30 days; if new messages are generated before syncing, they will not be synced. + * Users A, B, C, D, and E: A and B are online, while C, D, and E are offline. + * 1. A and B chat, generating two messages: messageA and messageB. + * 2. A and B go offline. + * 3. C and D come online, generating two messages: messageC and messageD. + * 4. A and B come online, and C and D will push two messages, messageC and messageD, to A and B. However, A and B will not push messageA and messageB to C and D because C and D's latest message timestamps are earlier than A and B's. + * 5. E comes online, and A, B, C, and D will all push messages messageA, messageB, messageC, and messageD to E. + * + * Final results: + * A and B see 4 messages: messageC, messageD, messageA, and messageB. + * C and D see 2 messages: messageA and messageB. + * E sees 4 messages: messageA, messageB, messageC, and messageD. + * + * As shown above, C and D did not sync messages that were earlier than their own. + * On one hand, if we want to fully sync 30 days of messages, we must diff the timestamps of messages within 30 days and then insert them. The current implementation only does incremental additions, and messages will accumulate over time. + * For now, let's keep it this way and see if it's necessary to fully sync the data within 30 days later. + */ + const SendSyncHistoryMessageCommand = domain.command({ + name: 'Room.SendSyncHistoryMessageCommand', + impl: ({ get }, { peerId, lastMessageTime }: { peerId: string; lastMessageTime: number }) => { + const self = get(SelfUserQuery()) + console.log('SendSyncHistoryMessageCommand', peerId, peerRoom.peerId) + + const historyMessages = get(messageListDomain.query.ListQuery()).filter( + (message) => + message.type === MessageType.Normal && + message.worldTime > lastMessageTime && + message.worldTime - Date.now() <= SYNC_HISTORY_MAX_DAYS * 24 * 60 * 60 * 1000 + ) + + /** + * Message chunking to ensure that each message does not exceed WEB_RTC_MAX_MESSAGE_SIZE + * If the message itself exceeds the size limit, skip syncing that message directly. + */ + const pushHistoryMessageList = historyMessages.reduce((acc, cur) => { + const pushHistoryMessage: SyncHistoryMessage = { + ...self, + id: nanoid(), + sendTime: Date.now(), + worldTime: Date.now(), + type: SendType.SyncHistory, + messages: [cur as NormalMessage] + } + const pushHistoryMessageByteSize = getTextByteSize(JSON.stringify(pushHistoryMessage)) + + if (pushHistoryMessageByteSize < WEB_RTC_MAX_MESSAGE_SIZE) { + if (acc.length) { + const mergedSize = getTextByteSize(JSON.stringify(acc[acc.length - 1])) + pushHistoryMessageByteSize + if (mergedSize < WEB_RTC_MAX_MESSAGE_SIZE) { + acc[acc.length - 1].messages.push(cur as NormalMessage) + } else { + acc.push(pushHistoryMessage) + } + } else { + acc.push(pushHistoryMessage) + } + } + return acc + }, []) + + return pushHistoryMessageList.map((message) => { + peerRoom.sendMessage(message, peerId) + return SendSyncHistoryMessageEvent(message) + }) } }) @@ -234,8 +350,12 @@ const RoomDomain = Remesh.domain({ } }) - const SendJoinMessageEvent = domain.event({ - name: 'Room.SendJoinMessageEvent' + const SendSyncHistoryMessageEvent = domain.event({ + name: 'Room.SendSyncHistoryMessageEvent' + }) + + const SendSyncUserMessageEvent = domain.event({ + name: 'Room.SendSyncUserMessageEvent' }) const SendTextMessageEvent = domain.event({ @@ -287,7 +407,7 @@ const RoomDomain = Remesh.domain({ if (peerRoom.peerId === peerId) { return [OnJoinRoomEvent(peerId)] } else { - return [SendJoinMessageCommand(peerId), OnJoinRoomEvent(peerId)] + return [SendSyncUserMessageCommand(peerId), OnJoinRoomEvent(peerId)] } }) ) @@ -308,16 +428,19 @@ const RoomDomain = Remesh.domain({ const messageCommand$ = (() => { switch (message.type) { - case SendType.Join: { + case SendType.SyncUser: { const userList = get(UserListQuery()) - const selfUser = userList.find((user) => user.peerId === peerRoom.peerId)! + const selfUser = get(SelfUserQuery()) // If the browser has multiple tabs open, it can cause the same user to join multiple times with the same peerId but different userId - const isSelfJoinEvent = !!userList.find((user) => user.userId === message.userId) + const isRepeatJoin = userList.some((user) => user.userId === message.userId) // When a new user joins, it triggers join events for all users, i.e., newUser join event and oldUser join event // Use joinTime to determine if it's a new user const isNewJoinEvent = selfUser.joinTime < message.joinTime - return isSelfJoinEvent + const lastMessageTime = get(LastMessageTimeQuery()) + const needSyncHistory = lastMessageTime > message.lastMessageTime + + return isRepeatJoin ? EMPTY : of( UpdateUserListCommand({ type: 'create', user: message }), @@ -327,17 +450,29 @@ const RoomDomain = Remesh.domain({ id: nanoid(), body: `"${message.username}" joined the chat`, type: MessageType.Prompt, - date: Date.now() + receiveTime: Date.now() + }) + : null, + needSyncHistory + ? SendSyncHistoryMessageCommand({ + peerId: message.peerId, + lastMessageTime: message.lastMessageTime }) : null ) } + + case SendType.SyncHistory: { + toast.success('Syncing history messages.') + return of(...message.messages.map((message) => messageListDomain.command.UpsertItemCommand(message))) + } + case SendType.Text: return of( messageListDomain.command.CreateItemCommand({ ...message, type: MessageType.Normal, - date: Date.now(), + receiveTime: Date.now(), likeUsers: [], hateUsers: [] }) @@ -348,7 +483,7 @@ const RoomDomain = Remesh.domain({ return EMPTY } const _message = get(messageListDomain.query.ItemQuery(message.id)) as NormalMessage - const type = message.type === 'like' ? 'likeUsers' : 'hateUsers' + const type = message.type === 'Like' ? 'likeUsers' : 'hateUsers' return of( messageListDomain.command.UpdateItemCommand({ ..._message, @@ -382,7 +517,7 @@ const RoomDomain = Remesh.domain({ impl: ({ get }) => { const onLeaveRoom$ = fromEventPattern(peerRoom.onLeaveRoom).pipe( map((peerId) => { - // console.log('onLeaveRoom', peerId) + console.log('onLeaveRoom', peerId, get(SelfUserQuery()).peerId) const user = get(UserListQuery()).find((user) => user.peerId === peerId) if (user) { @@ -393,7 +528,9 @@ const RoomDomain = Remesh.domain({ id: nanoid(), body: `"${user.username}" left the chat`, type: MessageType.Prompt, - date: Date.now() + sendTime: Date.now(), + worldTime: Date.now(), + receiveTime: Date.now() }), OnLeaveRoomEvent(peerId) ] @@ -425,6 +562,8 @@ const RoomDomain = Remesh.domain({ impl: ({ get }) => { const beforeUnload$ = fromEvent(window, 'beforeunload').pipe( map(() => { + console.log('beforeunload') + return get(JoinStatusModule.query.IsFinishedQuery()) ? LeaveRoomCommand() : null }) ) @@ -444,13 +583,15 @@ const RoomDomain = Remesh.domain({ SendTextMessageCommand, SendLikeMessageCommand, SendHateMessageCommand, - SendJoinMessageCommand + SendSyncUserMessageCommand, + SendSyncHistoryMessageCommand }, event: { SendTextMessageEvent, SendLikeMessageEvent, SendHateMessageEvent, - SendJoinMessageEvent, + SendSyncUserMessageEvent, + SendSyncHistoryMessageEvent, JoinRoomEvent, LeaveRoomEvent, OnMessageEvent, diff --git a/src/domain/impls/PeerRoom2.ts b/src/domain/impls/PeerRoom2.ts index 64cd1c1..2336d70 100644 --- a/src/domain/impls/PeerRoom2.ts +++ b/src/domain/impls/PeerRoom2.ts @@ -5,6 +5,8 @@ import { stringToHex } from '@/utils' import { nanoid } from 'nanoid' import EventHub from '@resreq/event-hub' import { RoomMessage } from '../Room' +import { JSONR } from '@/utils' + export interface Config { peerId?: string roomId: string @@ -50,11 +52,11 @@ class PeerRoom extends EventHub { if (!this.room) { this.emit('error', new Error('Room not joined')) } else { - this.room.send(JSON.stringify(message), id) + this.room.send(JSONR.stringify(message)!, id) } }) } else { - this.room.send(JSON.stringify(message), id) + this.room.send(JSONR.stringify(message)!, id) } return this } @@ -65,11 +67,11 @@ class PeerRoom extends EventHub { if (!this.room) { this.emit('error', new Error('Room not joined')) } else { - this.room.on('message', (message) => callback(JSON.parse(message) as RoomMessage)) + this.room.on('message', (message) => callback(JSONR.parse(message) as RoomMessage)) } }) } else { - this.room.on('message', (message) => callback(JSON.parse(message) as RoomMessage)) + this.room.on('message', (message) => callback(JSONR.parse(message) as RoomMessage)) } return this } diff --git a/src/domain/impls/Storage.ts b/src/domain/impls/Storage.ts index f3047dc..fba5ebb 100644 --- a/src/domain/impls/Storage.ts +++ b/src/domain/impls/Storage.ts @@ -7,6 +7,7 @@ import { webExtensionDriver } from '@/utils/webExtensionDriver' import { Storage } from '@/domain/externs/Storage' import { EVENT } from '@/constants/event' +import { JSONR } from '@/utils' export const localStorage = createStorage({ driver: localStorageDriver({ base: `${STORAGE_NAME}:` }) @@ -22,8 +23,8 @@ export const browserSyncStorage = createStorage({ export const LocalStorageImpl = LocalStorageExtern.impl({ name: STORAGE_NAME, - get: localStorage.getItem, - set: localStorage.setItem, + get: async (key) => JSONR.parse(await localStorage.getItem(key)), + set: (key, value) => localStorage.setItem(key, JSONR.stringify(value)!), remove: localStorage.removeItem, clear: localStorage.clear, watch: async (callback) => { @@ -45,8 +46,8 @@ export const LocalStorageImpl = LocalStorageExtern.impl({ export const IndexDBStorageImpl = IndexDBStorageExtern.impl({ name: STORAGE_NAME, - get: indexDBStorage.getItem, - set: indexDBStorage.setItem, + get: async (key) => JSONR.parse(await indexDBStorage.getItem(key)), + set: (key, value) => indexDBStorage.setItem(key, JSONR.stringify(value)), remove: indexDBStorage.removeItem, clear: indexDBStorage.clear, watch: indexDBStorage.watch as Storage['watch'], @@ -55,8 +56,8 @@ export const IndexDBStorageImpl = IndexDBStorageExtern.impl({ export const BrowserSyncStorageImpl = BrowserSyncStorageExtern.impl({ name: STORAGE_NAME, - get: browserSyncStorage.getItem, - set: browserSyncStorage.setItem, + get: async (key) => JSONR.parse(await browserSyncStorage.getItem(key)), + set: (key, value) => browserSyncStorage.setItem(key, JSONR.stringify(value)), remove: browserSyncStorage.removeItem, clear: browserSyncStorage.clear, watch: browserSyncStorage.watch as Storage['watch'], diff --git a/src/utils/getTextByteSize.ts b/src/utils/getTextByteSize.ts new file mode 100644 index 0000000..8bafcb8 --- /dev/null +++ b/src/utils/getTextByteSize.ts @@ -0,0 +1,3 @@ +export const getTextByteSize = (text: string) => { + return new TextEncoder().encode(text).length +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 05865ff..d5e36d2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,3 +15,5 @@ export { default as getCursorPosition } from './getCursorPosition' export { default as getTextSimilarity } from './getTextSimilarity' export { default as getRootNode } from './getRootNode' export { default as blobToBase64 } from './blobToBase64' +export * as JSONR from './jsonr' +export { getTextByteSize } from './getTextByteSize' diff --git a/src/utils/jsonr.ts b/src/utils/jsonr.ts new file mode 100644 index 0000000..cc24233 --- /dev/null +++ b/src/utils/jsonr.ts @@ -0,0 +1,10 @@ +import JSONR from '@perfsee/jsonr' +import { isNullish } from '@/utils' + +export const parse = (value: string | number | boolean | null): T | null => { + return !isNullish(value) ? JSONR.parse(value!.toString()) : null +} + +export const stringify = (value: any): string | null => { + return !isNullish(value) ? JSONR.stringify(value) : null +}