Skip to content

Commit

Permalink
add compression to public view
Browse files Browse the repository at this point in the history
  • Loading branch information
velascoandres committed Mar 30, 2024
1 parent f5ec44a commit c03f714
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 89 deletions.
28 changes: 27 additions & 1 deletion src/app/_whiteboards/components/whiteboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
type ExcalidrawInitialDataState
} from '@excalidraw/excalidraw/types/types'

import { decompressContent } from '@/lib/compress-whiteboard'


export interface Content {
scene: {
Expand All @@ -31,6 +33,13 @@ interface Props {
onChange?: WhiteboardChangeEventHandler
}

interface RawContentProps extends Omit<Props, 'initialContent'> {
id: number
viewModeEnabled?: boolean
initialRawCompressed?: string
onChange?: WhiteboardChangeEventHandler
}

const Excalidraw = dynamic(
async () => (await import('@excalidraw/excalidraw')).Excalidraw,
{
Expand Down Expand Up @@ -96,4 +105,21 @@ export const Whiteboard = ({
</Excalidraw>
)
}



export const WhiterboardFromCompressed = ({ initialRawCompressed,
...rest
}: RawContentProps) => {

const [initialContent, setInitialContent] = useState<Content>()

useEffect(() => {
if (!initialRawCompressed){
return
}
void decompressContent(initialRawCompressed)
.then((content) => setInitialContent(content as unknown as Content))
}, [initialRawCompressed])

return <Whiteboard {...rest} initialContent={initialContent} />
}
7 changes: 2 additions & 5 deletions src/app/_whiteboards/hooks/use-whiteboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { type ExcalidrawElement } from '@excalidraw/excalidraw/types/element/typ
import { type AppState, type BinaryFiles } from '@excalidraw/excalidraw/types/types'

import { useDebounceCallback } from '@/app/_shared/hooks/use-debounce-callback'
import { b64encode } from '@/lib/base64'
import { compressStream, JSONtoStream, responseToBuffer } from '@/lib/compress'
import { compressContent } from '@/lib/compress-whiteboard'
import * as exportUtils from '@/lib/export-whiteboard'
import { api } from '@/trpc/react'

Expand Down Expand Up @@ -106,9 +105,7 @@ export const useWhiteboard = (id: number) => {
}
}

void compressStream(JSONtoStream(content))
.then(responseToBuffer)
.then(b64encode)
void compressContent(content)
.then((compressedRawContent) => updateContent({
id: currentWhiteboard.id,
compressedRawContent
Expand Down
4 changes: 2 additions & 2 deletions src/app/view-whiteboard/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { type Metadata,type ResolvingMetadata } from 'next'
import { redirect } from 'next/navigation'

import Loading from '@/app/(protected)/loading'
import findPublicWhiteboardById from '@/server/api/whiteboard/usecases/find-public-whiteboard'
import findWhiteboardInfo from '@/server/api/whiteboard/usecases/find-whiteboard-info'
import { db } from '@/server/db'


export async function generateMetadata({ params }: {params: {id: string}}, parent: ResolvingMetadata) {
const id = Number(params.id)

const whiteboard = await findPublicWhiteboardById(db, { id, omitContent: true })
const whiteboard = await findWhiteboardInfo(db, { id, isPublic: true })


if (!whiteboard){
Expand Down
19 changes: 9 additions & 10 deletions src/app/view-whiteboard/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,42 @@
import React from 'react'
import { redirect } from 'next/navigation'

import { type Content,Whiteboard } from '@/app/_whiteboards/components/whiteboard'
import { WhiterboardFromCompressed } from '@/app/_whiteboards/components/whiteboard'
import { WhiteboardHeader } from '@/app/_whiteboards/components/whiteboard-header'
import { type PublicWhiteboard } from '@/app/_whiteboards/interfaces/whiteboard'
import findPublicWhiteboardById from '@/server/api/whiteboard/usecases/find-public-whiteboard'
import findWhiteboardContent from '@/server/api/whiteboard/usecases/find-whiteboard-content'
import { db } from '@/server/db'


const getWhiteboard = async (id: number) => {
const whiteboard = await findPublicWhiteboardById(db, { id })
const whiteboard = await findWhiteboardContent(db, { id, isPublic: true })

if (!whiteboard){
redirect('/not-found')
}

return whiteboard as unknown as PublicWhiteboard & {content: undefined | Content}
return whiteboard
}



const WhitebardViewPage = async ({ params }: {params: {id: string}}) => {
const whiteboardId = Number(params.id)

const whiteboard: PublicWhiteboard = await getWhiteboard(whiteboardId)
const whiteboard = await getWhiteboard(whiteboardId)

return (
<main className="h-screen w-screen">
<Whiteboard
<WhiterboardFromCompressed
viewModeEnabled
id={whiteboard?.id}
initialContent={whiteboard?.content as Content}
initialRawCompressed={whiteboard.compressedRawContent}
/>
<WhiteboardHeader
title={whiteboard.name}
description={whiteboard.description ?? ''}
creator={{
name: whiteboard.createdBy.name,
avatarUrl: whiteboard.createdBy.image
name: whiteboard.createdBy.name ?? '',
avatarUrl: whiteboard.createdBy.image ?? ''
}}
/>
</main>
Expand Down
7 changes: 7 additions & 0 deletions src/dtos/whiteboard-dtos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from 'zod'

import { SearchByIdDto } from './shared-dtos'

export const CreateWhiteboardDto = z.object({
name: z.string().min(1, 'A name is required').max(30, 'The name must be a maximum of 30 characters.'),
description: z.string().max(180, 'The description must be a maximum of 180 characters.').optional(),
Expand All @@ -15,3 +17,8 @@ export const UpdateWhiteboardContentDto = z.object({
id: z.number(),
compressedRawContent: z.string(),
})


export const SearchWhitheboard = z.object({
isPublic: z.boolean().optional(),
}).merge(SearchByIdDto)
30 changes: 5 additions & 25 deletions src/lib/base64.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
export const b64encode = (arrayBuffer: ArrayBuffer) => {
const buffer = Buffer.from(arrayBuffer)
const base64String = buffer.toString('base64')

// export const b64encode = (buf: ArrayBuffer) => {
// return btoa(String.fromCharCode(...new Uint8Array(buf)))
// }

export const b64encode = (buffer: ArrayBuffer) => {
let binary = ''
const bytes = new Uint8Array(buffer)
const len = bytes.byteLength
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]!)
}

return window.btoa(binary)
return base64String
}

// export const b64decode = (str: string) => {
// const binary_string = window.atob(str)
// const len = binary_string.length
// const bytes = new Uint8Array(new ArrayBuffer(len))
// for (let i = 0; i < len; i++) {
// bytes[i] = binary_string.charCodeAt(i)
// }

// return bytes
// }

export const b64decode = (base64: string) => {
const buffer = Buffer.from(base64, 'base64')

return new Uint8Array(buffer)
}
}
21 changes: 21 additions & 0 deletions src/lib/compress-whiteboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { b64encode } from './base64'
import {
b64toStream,
compressStream,
decompressStream,
JSONtoStream,
responseToBuffer
} from './compress'

export const compressContent = async (content: Record<string, unknown>) => {
const compressedStream = await compressStream(JSONtoStream(content))
const compressedBuffer = await responseToBuffer(compressedStream)

return b64encode(compressedBuffer)
}

export const decompressContent = (base64: string): Promise<Record<string, unknown>> => {
const contentResponse = decompressStream(b64toStream(base64))

return contentResponse.json() as Promise<Record<string, unknown>>
}
42 changes: 0 additions & 42 deletions src/server/api/whiteboard/usecases/find-public-whiteboard.ts

This file was deleted.

51 changes: 51 additions & 0 deletions src/server/api/whiteboard/usecases/find-whiteboard-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { and, eq,type SQL } from 'drizzle-orm'
import { type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { type z } from 'zod'

import { type SearchWhitheboard } from '@/dtos/whiteboard-dtos'
import { compressContent } from '@/lib/compress-whiteboard'
import type * as schema from '@/server/db/schema'
import { whiteboards } from '@/server/db/schema'
import { NotFound } from '@/server/exceptions/not-found'


type Options = z.infer<typeof SearchWhitheboard>

const findWhiteboardContent = async (db: PostgresJsDatabase<typeof schema>, options: Options) => {
const { id, isPublic } = options

let baseFilter = eq(whiteboards.id, id)

if (isPublic !== undefined){
baseFilter = and(baseFilter, eq(whiteboards.isPublic, isPublic)) as SQL<typeof schema>
}

const currentWhiteboard = await db.query.whiteboards.findFirst({
where: baseFilter,
with: {
createdBy: {
columns: {
email: false,
emailVerified: false,
}
},
}
})

if (!currentWhiteboard){
throw new NotFound('Whiteboard not found')
}

const compressedRawContent = await compressContent(currentWhiteboard.content as Record<string, unknown>)

return {
id: currentWhiteboard.id,
name: currentWhiteboard.name,
compressedRawContent,
description: currentWhiteboard.description,
previewUrl: currentWhiteboard.previewUrl,
createdBy: currentWhiteboard.createdBy,
}
}

export default findWhiteboardContent
37 changes: 37 additions & 0 deletions src/server/api/whiteboard/usecases/find-whiteboard-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { and, eq,type SQL } from 'drizzle-orm'
import { type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { type z } from 'zod'

import { type SearchWhitheboard } from '@/dtos/whiteboard-dtos'
import type * as schema from '@/server/db/schema'
import { whiteboards } from '@/server/db/schema'
import { NotFound } from '@/server/exceptions/not-found'


type Options = z.infer<typeof SearchWhitheboard>

const findWhiteboardInfo = async (db: PostgresJsDatabase<typeof schema>, options: Options) => {
const { id, isPublic } = options

let baseFilter = eq(whiteboards.id, id)

if (isPublic !== undefined){
baseFilter = and(baseFilter, eq(whiteboards.isPublic, isPublic)) as SQL<typeof schema>
}

const currentWhiteboard = await db.query.whiteboards.findFirst({
where: baseFilter,
columns: {
content: false,
}
})

if (!currentWhiteboard){
throw new NotFound('Whiteboard not found')
}


return currentWhiteboard
}

export default findWhiteboardInfo
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { type z } from 'zod'

import { type UpdateWhiteboardContentDto } from '@/dtos/whiteboard-dtos'
import { b64toStream, decompressStream } from '@/lib/compress'
import { decompressContent } from '@/lib/compress-whiteboard'
import type * as schema from '@/server/db/schema'
import { whiteboards } from '@/server/db/schema'
import { NotAuthorized } from '@/server/exceptions/not-authorized'
Expand All @@ -29,9 +29,7 @@ const updateWhiteboardContent = async (db: PostgresJsDatabase<typeof schema>, op
if (!isOwner){
throw new NotAuthorized('User not related to whiteboard')
}

const contentResponse = decompressStream(b64toStream(compressedRawContent))
const content = await contentResponse.json() as Record<string, unknown>
const content = await decompressContent(compressedRawContent)

return db.update(whiteboards).set({
content
Expand Down

0 comments on commit c03f714

Please sign in to comment.