Skip to content

Commit

Permalink
Initialise shapes with server data automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
balegas committed Oct 25, 2024
1 parent dd64a75 commit 946f1b7
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 34 deletions.
42 changes: 11 additions & 31 deletions examples/nextjs-ssr-example/app/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
"use client"

import { v4 as uuidv4 } from "uuid"
import {
useShape,
getShapeStream,
SerializedShapeData,
} from "@electric-sql/react"
import { useShape, getShapeStream } from "@electric-sql/react"
import "./Example.css"
import { matchStream } from "./match-stream"
import { Offset, ShapeStreamOptions } from "@electric-sql/client"
import { Row, ShapeStream, ShapeStreamOptions } from "@electric-sql/client"
import { useOptimistic } from "react"

const ELECTRIC_URL = process.env.ELECTRIC_URL || "http://localhost:3000"
const ELECTRIC_URL = process.env.ELECTRIC_URL || `http://localhost:3000`

const parser = {
timestamptz: (date: string) => new Date(date).getTime(),
}

const shapePosition: { shapeId?: string; offset?: Offset } = {
shapeId: undefined,
offset: `-1`,
}

const shapeOptions: () => ShapeStreamOptions = () => {
const shapeOptions: () => ShapeStreamOptions<Row> = () => {
if (typeof window !== `undefined`) {
return {
url: new URL(`/shape-proxy/items`, window?.location.origin).href,
Expand All @@ -37,17 +28,12 @@ const shapeOptions: () => ShapeStreamOptions = () => {
}
}

const itemShape = () => ({ ...shapeOptions(), ...shapePosition })

const updateShapePosition = (offset: Offset, shapeId?: string) => {
shapePosition.offset = offset
shapePosition.shapeId = shapeId
}

type Item = { id: string; created_at: number }
const itemShape = (): ShapeStreamOptions<Row> => ({ ...shapeOptions() })

async function createItem(newId: string) {
const itemsStream = getShapeStream(itemShape())
// FIX types later
const itemsStream = getShapeStream(itemShape()) as unknown as ShapeStream<Row>

// Match the insert
const findUpdatePromise = matchStream({
Expand All @@ -66,7 +52,9 @@ async function createItem(newId: string) {
}

async function clearItems() {
const itemsStream = getShapeStream(itemShape())
// FIX types later
const itemsStream = getShapeStream(itemShape()) as unknown as ShapeStream<Row>

// Match the delete
const findUpdatePromise = matchStream({
stream: itemsStream,
Expand All @@ -82,17 +70,9 @@ async function clearItems() {
return await Promise.all([findUpdatePromise, fetchPromise])
}

export default function Home({
shapes,
}: {
shapes: { items: SerializedShapeData }
}) {
const { shapeId, offset, data } = shapes.items
updateShapePosition(offset, shapeId)

export default function Home() {
const { data: items } = useShape({
...itemShape(),
shapeData: new Map(Object.entries(data ?? new Map())),
parser,
}) as unknown as {
data: Item[]
Expand Down
11 changes: 9 additions & 2 deletions examples/nextjs-ssr-example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import React from "react"
import { getSerializedShape } from "@electric-sql/react"
import Home from "./Home"
import SSRShapesInitializer from "./ssr-shapes-provider"

const serverShapeOptions = {
const serverOptions = {
url: new URL(`http://localhost:3000/v1/shape/items`).href,
}

const Page = async () => {
return <Home shapes={{ items: getSerializedShape(serverShapeOptions) }} />
const data = getSerializedShape(serverOptions)

return (
<SSRShapesInitializer serializedShapes={[{ data, serverOptions }]}>
<Home />
</SSRShapesInitializer>
)
}

export default Page
43 changes: 43 additions & 0 deletions examples/nextjs-ssr-example/app/ssr-shapes-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client"

import { createContext } from "react"
import { SerializedShapeData, useShape } from "@electric-sql/react"
import { ShapeStreamOptions } from "@electric-sql/client/*"

export const SSRShapesContext = createContext({})

type SerializedShape = {
serverOptions: ShapeStreamOptions
data: SerializedShapeData
}

const getClientBaseUrl = () => window?.location.origin

export default function SSRShapesInitializer({
children,
serializedShapes,
}: {
children: React.ReactNode
serializedShapes: SerializedShape[]
}) {
if (typeof window === `undefined`) {
return children
}

for (const { serverOptions, data } of serializedShapes) {
// FIX client url
const clientUrl = new URL(`/shape-proxy/items`, getClientBaseUrl()).href
const shapeOptions = {
...serverOptions,
url: clientUrl,
offset: data.offset,
shapeId: data.shapeId,
}

const shapeData = new Map(Object.entries(data.data ?? new Map()))
/* eslint-disable react-hooks/rules-of-hooks */
useShape({ ...shapeOptions, shapeData })
}

return children
}
9 changes: 8 additions & 1 deletion packages/react-hooks/src/react-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ export async function preloadShape<T extends Row<unknown> = Row>(
}

export function sortedOptionsHash<T>(options: ShapeStreamOptions<T>): string {
return JSON.stringify(options, Object.keys(options).sort())
// Filter options that uniquely identify the shape. DISCUSS BEFORE MERGING
const uniqueShapeOptions = {
url: options.url,
where: options.where,
columns: options.columns,
headers: options.headers,
}
return JSON.stringify(uniqueShapeOptions, Object.keys(options).sort())
}

export function getShapeStream<T extends Row<unknown>>(
Expand Down

0 comments on commit 946f1b7

Please sign in to comment.