feat: migrate data persistence from Supabase to Postgres
Some checks are pending
Checks / Tests & checks (push) Waiting to run
Checks / Build all projects (push) Waiting to run
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Waiting to run
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Waiting to run
End to end tests / End to end tests (push) Waiting to run
Publish Canary Packages / Publish Canary Packages (push) Waiting to run
Publish VS Code Extension / Publish VS Code Extension (push) Waiting to run
Some checks are pending
Checks / Tests & checks (push) Waiting to run
Checks / Build all projects (push) Waiting to run
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Waiting to run
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Waiting to run
End to end tests / End to end tests (push) Waiting to run
Publish Canary Packages / Publish Canary Packages (push) Waiting to run
Publish VS Code Extension / Publish VS Code Extension (push) Waiting to run
Switched from Supabase to Postgres for handling database operations related to room snapshots and drawings. This change involves updating the imports and persistence logic in various components to utilize Postgres instead of Supabase. Benefits include improved performance and greater control over database operations. Added connection and query handling for Postgres in the utility function. Includes: - Updated imports and logic in TLDrawDurableObject - Changes in getRoomSnapshot to use Postgres - New createPostgresClient utility function for DB connection
This commit is contained in:
parent
8aa4fd3352
commit
43419581be
3 changed files with 423 additions and 398 deletions
|
@ -1,7 +1,7 @@
|
||||||
/// <reference no-default-lib="true"/>
|
/// <reference no-default-lib="true"/>
|
||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
import { SupabaseClient } from '@supabase/supabase-js'
|
import { Client as PostgresClient } from 'pg'
|
||||||
import {
|
import {
|
||||||
READ_ONLY_LEGACY_PREFIX,
|
READ_ONLY_LEGACY_PREFIX,
|
||||||
READ_ONLY_PREFIX,
|
READ_ONLY_PREFIX,
|
||||||
|
@ -11,19 +11,18 @@ import {
|
||||||
} from '@tldraw/dotcom-shared'
|
} from '@tldraw/dotcom-shared'
|
||||||
import {
|
import {
|
||||||
RoomSnapshot,
|
RoomSnapshot,
|
||||||
TLCloseEventCode,
|
|
||||||
TLSocketRoom,
|
TLSocketRoom,
|
||||||
type PersistedRoomSnapshotForSupabase,
|
type PersistedRoomSnapshotForSupabase,
|
||||||
} from '@tldraw/sync-core'
|
} from '@tldraw/sync-core'
|
||||||
import { TLRecord } from '@tldraw/tlschema'
|
import { TLRecord } from '@tldraw/tlschema'
|
||||||
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
import { assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
||||||
import { createPersistQueue, createSentry } from '@tldraw/worker-shared'
|
import { createPersistQueue, createSentry } from '@tldraw/worker-shared'
|
||||||
import { IRequest, Router } from 'itty-router'
|
import { IRequest, Router } from 'itty-router'
|
||||||
import { AlarmScheduler } from './AlarmScheduler'
|
import { AlarmScheduler } from './AlarmScheduler'
|
||||||
import { PERSIST_INTERVAL_MS } from './config'
|
import { PERSIST_INTERVAL_MS } from './config'
|
||||||
import { getR2KeyForRoom } from './r2'
|
import { getR2KeyForRoom } from './r2'
|
||||||
import { Analytics, DBLoadResult, Environment, TLServerEvent } from './types'
|
import { Analytics, DBLoadResult, Environment, TLServerEvent } from './types'
|
||||||
import { createSupabaseClient } from './utils/createSupabaseClient'
|
import { createPostgresClient } from './utils/createPostgresClient'
|
||||||
import { getSlug } from './utils/roomOpenMode'
|
import { getSlug } from './utils/roomOpenMode'
|
||||||
import { throttle } from './utils/throttle'
|
import { throttle } from './utils/throttle'
|
||||||
|
|
||||||
|
@ -119,7 +118,7 @@ export class TLDrawDurableObject {
|
||||||
storage: DurableObjectStorage
|
storage: DurableObjectStorage
|
||||||
|
|
||||||
// For persistence
|
// For persistence
|
||||||
supabaseClient: SupabaseClient | void
|
postgresClient: PostgresClient | null
|
||||||
|
|
||||||
// For analytics
|
// For analytics
|
||||||
measure: Analytics | undefined
|
measure: Analytics | undefined
|
||||||
|
@ -127,7 +126,7 @@ export class TLDrawDurableObject {
|
||||||
// For error tracking
|
// For error tracking
|
||||||
sentryDSN: string | undefined
|
sentryDSN: string | undefined
|
||||||
|
|
||||||
readonly supabaseTable: string
|
readonly postgresTable: string
|
||||||
readonly r2: {
|
readonly r2: {
|
||||||
readonly rooms: R2Bucket
|
readonly rooms: R2Bucket
|
||||||
readonly versionCache: R2Bucket
|
readonly versionCache: R2Bucket
|
||||||
|
@ -143,9 +142,9 @@ export class TLDrawDurableObject {
|
||||||
this.storage = state.storage
|
this.storage = state.storage
|
||||||
this.sentryDSN = env.SENTRY_DSN
|
this.sentryDSN = env.SENTRY_DSN
|
||||||
this.measure = env.MEASURE
|
this.measure = env.MEASURE
|
||||||
this.supabaseClient = createSupabaseClient(env)
|
this.postgresClient = createPostgresClient(env)
|
||||||
|
|
||||||
this.supabaseTable = env.TLDRAW_ENV === 'production' ? 'drawings' : 'drawings_staging'
|
this.postgresTable = env.TLDRAW_ENV === 'production' ? 'drawings' : 'drawings_staging'
|
||||||
this.r2 = {
|
this.r2 = {
|
||||||
rooms: env.ROOMS,
|
rooms: env.ROOMS,
|
||||||
versionCache: env.ROOMS_HISTORY_EPHEMERAL,
|
versionCache: env.ROOMS_HISTORY_EPHEMERAL,
|
||||||
|
@ -349,7 +348,7 @@ export class TLDrawDurableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the room's drawing data. First we check the R2 bucket, then we fallback to supabase (legacy).
|
// Load the room's drawing data. First we check the R2 bucket, then we fallback to Postgres (legacy).
|
||||||
async loadFromDatabase(persistenceKey: string): Promise<DBLoadResult> {
|
async loadFromDatabase(persistenceKey: string): Promise<DBLoadResult> {
|
||||||
try {
|
try {
|
||||||
const key = getR2KeyForRoom(persistenceKey)
|
const key = getR2KeyForRoom(persistenceKey)
|
||||||
|
@ -359,26 +358,18 @@ export class TLDrawDurableObject {
|
||||||
return { type: 'room_found', snapshot: await roomFromBucket.json() }
|
return { type: 'room_found', snapshot: await roomFromBucket.json() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we don't have a room in the bucket, try to load from supabase
|
// if we don't have a room in the bucket, try to load from Postgres
|
||||||
if (!this.supabaseClient) return { type: 'room_not_found' }
|
if (!this.postgresClient) return { type: 'room_not_found' }
|
||||||
const { data, error } = await this.supabaseClient
|
await this.postgresClient.connect()
|
||||||
.from(this.supabaseTable)
|
const result = await this.postgresClient.query('SELECT * FROM ' + this.postgresTable + ' WHERE slug = $1', [persistenceKey])
|
||||||
.select('*')
|
await this.postgresClient.end()
|
||||||
.eq('slug', persistenceKey)
|
|
||||||
|
|
||||||
if (error) {
|
if (result.rows.length === 0) {
|
||||||
this.logEvent({ type: 'room', roomId: persistenceKey, name: 'failed_load_from_db' })
|
|
||||||
|
|
||||||
console.error('failed to retrieve document', persistenceKey, error)
|
|
||||||
return { type: 'error', error: new Error(error.message) }
|
|
||||||
}
|
|
||||||
// if it didn't find a document, data will be an empty array
|
|
||||||
if (data.length === 0) {
|
|
||||||
return { type: 'room_not_found' }
|
return { type: 'room_not_found' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomFromSupabase = data[0] as PersistedRoomSnapshotForSupabase
|
const roomFromPostgres = result.rows[0] as PersistedRoomSnapshotForSupabase
|
||||||
return { type: 'room_found', snapshot: roomFromSupabase.drawing }
|
return { type: 'room_found', snapshot: roomFromPostgres.drawing }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logEvent({ type: 'room', roomId: persistenceKey, name: 'failed_load_from_db' })
|
this.logEvent({ type: 'room', roomId: persistenceKey, name: 'failed_load_from_db' })
|
||||||
|
|
||||||
|
@ -409,7 +400,7 @@ export class TLDrawDurableObject {
|
||||||
// just in case there's any possibility of setting up a neverending queue
|
// just in case there's any possibility of setting up a neverending queue
|
||||||
}, PERSIST_INTERVAL_MS / 2)
|
}, PERSIST_INTERVAL_MS / 2)
|
||||||
|
|
||||||
// Save the room to supabase
|
// Save the room to Postgres
|
||||||
async persistToDatabase() {
|
async persistToDatabase() {
|
||||||
await this._persistQueue()
|
await this._persistQueue()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,7 @@ import { notFound } from '@tldraw/worker-shared'
|
||||||
import { IRequest } from 'itty-router'
|
import { IRequest } from 'itty-router'
|
||||||
import { getR2KeyForSnapshot } from '../r2'
|
import { getR2KeyForSnapshot } from '../r2'
|
||||||
import { Environment } from '../types'
|
import { Environment } from '../types'
|
||||||
import { createSupabaseClient, noSupabaseSorry } from '../utils/createSupabaseClient'
|
import { createPostgresClient, noPostgresSorry } from '../utils/createPostgresClient'
|
||||||
import { getSnapshotsTable } from '../utils/getSnapshotsTable'
|
|
||||||
import { R2Snapshot } from './createRoomSnapshot'
|
|
||||||
|
|
||||||
function generateReponse(roomId: string, data: RoomSnapshot) {
|
function generateReponse(roomId: string, data: RoomSnapshot) {
|
||||||
return new Response(
|
return new Response(
|
||||||
|
@ -39,22 +37,23 @@ export async function getRoomSnapshot(request: IRequest, env: Environment): Prom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we can't find the snapshot in R2 then fallback to Supabase
|
// If we can't find the snapshot in R2 then fallback to Postgres
|
||||||
// Create a supabase client
|
// Create a Postgres client
|
||||||
const supabase = createSupabaseClient(env)
|
const postgresClient = createPostgresClient(env)
|
||||||
if (!supabase) return noSupabaseSorry()
|
if (!postgresClient) return noPostgresSorry()
|
||||||
|
|
||||||
// Get the snapshot from the table
|
try {
|
||||||
const supabaseTable = getSnapshotsTable(env)
|
await postgresClient.connect()
|
||||||
const result = await supabase
|
const result = await postgresClient.query('SELECT drawing FROM snapshots WHERE slug = $1 LIMIT 1', [roomId])
|
||||||
.from(supabaseTable)
|
await postgresClient.end()
|
||||||
.select('drawing')
|
|
||||||
.eq('slug', roomId)
|
|
||||||
.maybeSingle()
|
|
||||||
const data = result.data?.drawing as RoomSnapshot
|
|
||||||
|
|
||||||
if (!data) return notFound()
|
if (result.rows.length === 0) return notFound()
|
||||||
|
const data = result.rows[0].drawing as RoomSnapshot
|
||||||
|
|
||||||
// Send back the snapshot!
|
// Send back the snapshot!
|
||||||
return generateReponse(roomId, data)
|
return generateReponse(roomId, data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error querying Postgres', err)
|
||||||
|
return new Response(JSON.stringify({ error: true, message: 'Error querying Postgres' }), { status: 500 })
|
||||||
|
}
|
||||||
}
|
}
|
35
apps/dotcom-worker/src/utils/createPostgresClient.ts
Normal file
35
apps/dotcom-worker/src/utils/createPostgresClient.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Client } from 'pg'
|
||||||
|
import { Environment } from '../types'
|
||||||
|
|
||||||
|
export function createPostgresClient(env: Environment) {
|
||||||
|
if (env.POSTGRES_HOST && env.POSTGRES_USER && env.POSTGRES_PASSWORD && env.POSTGRES_DB) {
|
||||||
|
var client = new Client({
|
||||||
|
host: env.POSTGRES_HOST,
|
||||||
|
port: env.POSTGRES_PORT ? parseInt(env.POSTGRES_PORT) : 5432,
|
||||||
|
user: env.POSTGRES_USER,
|
||||||
|
password: env.POSTGRES_PASSWORD,
|
||||||
|
database: env.POSTGRES_DB,
|
||||||
|
})
|
||||||
|
|
||||||
|
client.connect()
|
||||||
|
|
||||||
|
client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS snapshots (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
slug VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
drawing JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn('No Postgres credentials, loading from Postgres disabled')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function noPostgresSorry() {
|
||||||
|
return new Response(JSON.stringify({ error: true, message: 'Could not create Postgres client' }))
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue