Skip to content

Commit

Permalink
Improved DX
Browse files Browse the repository at this point in the history
  • Loading branch information
balegas committed Nov 8, 2024
1 parent de124b8 commit 2d8330e
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 98 deletions.
37 changes: 10 additions & 27 deletions examples/nextjs-ssr-example/app/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,23 @@ import { v4 as uuidv4 } from "uuid"
import { useShape, getShapeStream } from "@electric-sql/react"
import "./Example.css"
import { matchStream } from "./match-stream"
import { Row, ShapeStream, ShapeStreamOptions } from "@electric-sql/client"
import { Row, ShapeStreamOptions } from "@electric-sql/client"
import { useOptimistic } from "react"

const ELECTRIC_URL = process.env.ELECTRIC_URL || `http://localhost:3000`
import { getProxiedOptions, getUrl } from "./utils"

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

const shapeOptions: () => ShapeStreamOptions<Row> = () => {
if (typeof window !== `undefined`) {
return {
url: new URL(`/shape-proxy/items`, window?.location.origin).href,
}
} else {
const controller = new AbortController()
controller.abort()
return {
url: new URL(`/v1/items`, ELECTRIC_URL).href,
signal: controller.signal,
}
}
}

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

const options: Partial<ShapeStreamOptions> = {
table: `items`,
parser,
}

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

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

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

// Match the delete
const findUpdatePromise = matchStream({
Expand All @@ -71,10 +57,7 @@ async function clearItems() {
}

export default function Home() {
const { data: items } = useShape({
...itemShape(),
parser,
}) as unknown as {
const { data: items } = useShape(getProxiedOptions(options)) as unknown as {
data: Item[]
}

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

import { cacheShapeState } from "@electric-sql/react"
import { getUrl, SerializedShape } from "./utils"

export default function ClientShapeProvider({
children,
serializedShapes,
}: {
children: React.JSX.Element
serializedShapes: SerializedShape[]
}) {
for (const { options, data } of serializedShapes) {
const newShapeOptions = { ...options, url: getUrl() }
cacheShapeState(newShapeOptions, data)
}
return children
}
22 changes: 9 additions & 13 deletions examples/nextjs-ssr-example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import React from "react"
import { getSerializedShape } from "@electric-sql/react"
import Home from "./Home"
import SSRShapesInitializer from "./ssr-shapes-provider"
import ServerShapeProvider from "./server-shape-provider"
import { ShapeDefintion } from "./utils"

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

const Page = async () => {
const data = getSerializedShape(serverOptions)

return (
<SSRShapesInitializer serializedShapes={[{ data, serverOptions }]}>
<Home />
</SSRShapesInitializer>
)
}
const Page = async () => (
<ServerShapeProvider options={[itemsShape]}>
<Home />
</ServerShapeProvider>
)

export default Page
39 changes: 39 additions & 0 deletions examples/nextjs-ssr-example/app/server-shape-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getSerializedShape } from "@electric-sql/react"
import React from "react"
import ClientShapeProvider from "./client-shapes-provider"
import { getUrl, SerializedShape, ShapeDefintion } from "./utils"

export default function ServerShapeProvider({
children,
options,
}: {
children: React.JSX.Element
options: ShapeDefintion[]
}) {
const clientShapes: SerializedShape[] = []
for (const shapeOptions of options) {
const serializedShape = getSerializedShape({
...shapeOptions,
url: getUrl(),
})

const clientOptions = {
table: shapeOptions.table,
columns: shapeOptions.columns,
where: shapeOptions.where,
shapeHandle: serializedShape.shapeHandle,
offset: serializedShape.offset,
}

clientShapes.push({
options: clientOptions,
data: serializedShape.data ?? new Map(),
})
}

return (
<ClientShapeProvider serializedShapes={clientShapes}>
{children}
</ClientShapeProvider>
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
export async function GET(
request: Request,
{ params }: { params: { table: string } }
) {
export async function GET(request: Request) {
const url = new URL(request.url)
const { table } = params
const originUrl = new URL(`http://localhost:3000/v1/shape/${table}`)
const originUrl = new URL(`http://localhost:3000/v1/shape`)
url.searchParams.forEach((value, key) => {
originUrl.searchParams.set(key, value)
})
Expand Down
43 changes: 0 additions & 43 deletions examples/nextjs-ssr-example/app/ssr-shapes-provider.ts

This file was deleted.

44 changes: 44 additions & 0 deletions examples/nextjs-ssr-example/app/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
Offset,
Shape,
ShapeData,
ShapeStreamOptions,
} from "@electric-sql/client/*"

export type ShapeDefintion = {
table: string
columns?: string[]
where?: string
}

export type ShapeDefinitionWithPosiotion = ShapeDefintion & {
offset: Offset
shapeHandle: string | undefined
}

export type SerializedShape = {
options: ShapeDefinitionWithPosiotion
data: ShapeData
}

export function getUrl() {
if (typeof window === `undefined`) {
return `${process.env.ELECTRIC_URL || `http://localhost:3000`}/v1/shape`
}
return `${window?.location.origin}/shape-proxy/v1/shape`
}

export function getProxiedOptions(
options: Omit<ShapeStreamOptions, "url">
): ShapeStreamOptions {
// ensure shape is not syncing on the server
const serverOptions: Partial<ShapeStreamOptions> = {}
if (typeof window === `undefined`) {
const controller = new AbortController()
controller.abort()
serverOptions.signal = controller.signal
serverOptions.subscribe = false
}

return { ...options, ...serverOptions, url: getUrl() }
}
26 changes: 17 additions & 9 deletions packages/react-hooks/src/react-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-s

type UnknownShape = Shape<Row<unknown>>
type UnknownShapeStream = ShapeStream<Row<unknown>>
export type SerializedShapeData = {
export type SerializedShape = {
offset: Offset
shapeId: string | undefined
data?: Record<string, unknown>
shapeHandle: string | undefined
data?: ShapeData<Row<unknown>>
}

const streamCache = new Map<string, UnknownShapeStream>()
Expand All @@ -34,8 +34,9 @@ export function sortedOptionsHash<T>(options: ShapeStreamOptions<T>): string {
// Filter options that uniquely identify the shape. DISCUSS BEFORE MERGING
const uniqueShapeOptions = {
url: options.url,
where: options.where,
table: options.table,
columns: options.columns,
where: options.where,
headers: options.headers,
}
return JSON.stringify(uniqueShapeOptions, Object.keys(options).sort())
Expand Down Expand Up @@ -149,13 +150,12 @@ export function useShape<
Selection = UseShapeResult<SourceData>,
>({
selector = identity as (arg: UseShapeResult<SourceData>) => Selection,
shapeData: data,
...options
}: UseShapeOptions<SourceData, Selection>): Selection {
const shapeStream = getShapeStream<SourceData>(
options as ShapeStreamOptions<GetExtensions<SourceData>>
)
const shape = getShape<SourceData>(shapeStream, data)
const shape = getShape<SourceData>(shapeStream)

const useShapeData = React.useMemo(() => {
let latestShapeData = parseShapeData(shape)
Expand All @@ -180,12 +180,20 @@ export function useShape<
}
export function getSerializedShape(
options: ShapeStreamOptions
): SerializedShapeData {
): SerializedShape {
const shapeStream = getShapeStream(options)
const shape = getShape(shapeStream)
return {
shapeId: shapeStream.shapeId,
shapeHandle: shapeStream.shapeHandle,
offset: shapeStream.offset,
data: Object.fromEntries(shape.valueSync),
data: shape.currentValue,
}
}

export function cacheShapeState(
options: ShapeStreamOptions,
shapeData: ShapeData
) {
const shapeStream = getShapeStream(options)
getShape(shapeStream, shapeData)
}

0 comments on commit 2d8330e

Please sign in to comment.