Skip to content

Commit 15ced65

Browse files
authored
Use tinybase for data sync (#18)
This removes the custom sync protocol in favor of tinybase, which has a more robust CRDT based protocol.
1 parent 150b0eb commit 15ced65

36 files changed

+418
-2251
lines changed

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
"@thisbeyond/solid-dnd": "0.7.5",
3434
"lib": "workspace:*",
3535
"motion": "11.15.0",
36+
"reconnecting-websocket": "^4.4.0",
3637
"solid-js": "1.9.3",
3738
"solid-motionone": "1.0.2",
3839
"solid-transition-group": "0.2.3",
40+
"tinybase": "^5.4.4",
3941
"vinxi": "0.5.1",
4042
"workbox-window": "7.3.0",
4143
"zod": "3.24.1"
Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { useConnectivitySignal } from "@solid-primitives/connectivity"
22
import { Match, Switch } from "solid-js"
33

4-
export function ConnectionWarning(props: { isAuthenticated: boolean; isConnected: boolean }) {
4+
export function ConnectionWarning(props: { isConnected: boolean }) {
55
const isOnline = useConnectivitySignal()
66

77
const showOfflineWarning = () => !isOnline()
88
const showNotConnectedWarning = () => isOnline() && !props.isConnected
9-
const showAuthWarning = () => isOnline() && props.isConnected && !props.isAuthenticated
109

1110
return (
1211
<Switch>
@@ -27,18 +26,6 @@ export function ConnectionWarning(props: { isAuthenticated: boolean; isConnected
2726
<span class="block">Your changes will be synced when the server is back up again.</span>
2827
</div>
2928
</Match>
30-
31-
<Match when={showAuthWarning()}>
32-
<div
33-
class="mx-2 mb-4 bg-amber2 border border-amber7 text-sm text-amber11 px-4 py-3 rounded relative"
34-
role="alert"
35-
>
36-
<strong class="font-bold">Whoppa!</strong>
37-
<span class="block">
38-
Looks like the server has lost its authentication info. Contact your system admin to re-authenticate.
39-
</span>
40-
</div>
41-
</Match>
4229
</Switch>
4330
)
4431
}

packages/app/src/components/Home.tsx

Lines changed: 14 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useConnectivitySignal } from "@solid-primitives/connectivity"
21
import {
32
DragDropProvider,
43
DragDropSensors,
@@ -7,87 +6,28 @@ import {
76
SortableProvider,
87
closestCenter,
98
} from "@thisbeyond/solid-dnd"
10-
import { Client, EventQueue, ShoppingList, type ShoppingListEvent, type ShoppingListItem, trimAndUppercase } from "lib"
9+
import { type ShoppingListItem, trimAndUppercase } from "lib"
1110
import { animate } from "motion/mini"
12-
import { For, type JSX, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
13-
import { createStore, reconcile } from "solid-js/store"
11+
import { For, type JSX, Show, createSignal, onCleanup, onMount } from "solid-js"
1412
import { Motion, Presence } from "solid-motionone"
1513
import { TransitionGroup } from "solid-transition-group"
16-
import { ConnectionWarning } from "~/components/ConnectionWarning"
1714
import { ItemRow } from "~/components/ItemRow"
18-
import { BrowserServerConnection } from "~/lib/browser-server-connection"
15+
import { isConnected, myShoppingList, shopping } from "~/lib/shopping-list"
1916
import { isInputField } from "~/lib/type-guards"
2017
import IconCaretRight from "~icons/radix-icons/caret-right"
21-
22-
function createClient() {
23-
const [connectionStatus, setConnectionStatus] = createSignal({ authenticated: true, connected: true })
24-
25-
const serverConnection = new BrowserServerConnection((value) => setConnectionStatus(value))
26-
27-
const initialShoppingListString = localStorage.getItem("main-shopping-list")
28-
const initialShoppingList = initialShoppingListString
29-
? (JSON.parse(initialShoppingListString) as ShoppingListItem[])
30-
: []
31-
32-
const [list, setStore] = createStore({ items: initialShoppingList })
33-
34-
const shoppingList = new ShoppingList(structuredClone(initialShoppingList), (newList) => {
35-
setStore("items", reconcile(structuredClone(newList)))
36-
localStorage.setItem("main-shopping-list", JSON.stringify(newList))
37-
})
38-
39-
const storedRemoteShoppingListCopyString = localStorage.getItem("remote-shopping-list")
40-
const storedRemoteShoppingListCopy = storedRemoteShoppingListCopyString
41-
? (JSON.parse(storedRemoteShoppingListCopyString) as ShoppingListItem[])
42-
: []
43-
const remoteShoppingListCopy = new ShoppingList(storedRemoteShoppingListCopy, (newList) => {
44-
localStorage.setItem("remote-shopping-list", JSON.stringify(newList))
45-
})
46-
47-
const storedEventQueueString = localStorage.getItem("event-queue")
48-
const storedEventQueue = storedEventQueueString ? (JSON.parse(storedEventQueueString) as ShoppingListEvent[]) : []
49-
const eventQueue = new EventQueue<ShoppingListEvent>(storedEventQueue, (events) => {
50-
localStorage.setItem("event-queue", JSON.stringify(events))
51-
})
52-
53-
const client = new Client({
54-
shoppingList,
55-
remoteShoppingListCopy,
56-
serverConnection,
57-
eventQueue,
58-
})
59-
60-
const isOnline = useConnectivitySignal()
61-
62-
createEffect(() => {
63-
if (isOnline()) client.connect()
64-
else serverConnection.disconnect()
65-
})
66-
67-
return { connectionStatus, client, items: list.items }
68-
}
18+
import { ConnectionWarning } from "./ConnectionWarning"
6919

7020
const ITEM_HEIGHT = 40
7121
const ITEM_HEIGHT_PX = `${ITEM_HEIGHT}px`
7222

7323
export function Home(props: { softwareKeyboardShown: boolean }) {
74-
const { connectionStatus, client, items } = createClient()
75-
76-
const sortedList = () => {
77-
return [...items].sort((a, b) => a.position - b.position)
78-
}
24+
const sortedList = () => myShoppingList.toSorted((a, b) => a.position - b.position)
7925

8026
const activeList = () => sortedList().filter((item) => !item.checked)
8127
const checkedList = () => sortedList().filter((item) => item.checked)
8228

8329
const [showChecked, setShowChecked] = createSignal(false)
8430

85-
const actions = {
86-
deleteItem: client.deleteItem.bind(client),
87-
setChecked: client.setItemChecked.bind(client),
88-
renameItem: client.renameItem.bind(client),
89-
}
90-
9131
const [activeItem, setActiveItem] = createSignal<ShoppingListItem | null>(null)
9232

9333
const onDragStart = ({ draggable }: DragEvent) => {
@@ -105,16 +45,16 @@ export function Home(props: { softwareKeyboardShown: boolean }) {
10545
const fromIndex = currentIds.indexOf(draggable.id as string)
10646
const toIndex = currentIds.indexOf(droppable.id as string)
10747

108-
const fromPosition = activeList()[fromIndex].position ?? fromIndex
109-
const toPosition = activeList()[toIndex].position ?? toIndex
48+
const fromPosition = activeList()[fromIndex].position
49+
const toPosition = activeList()[toIndex].position
11050

11151
if (fromIndex !== toIndex) {
112-
client.moveItem(draggable.id as string, { fromPosition, toPosition })
52+
shopping.moveItem(draggable.id as string, { fromPosition, toPosition })
11353
}
11454
}
11555
}
11656

117-
const ids = () => activeList().map(({ id }) => id)
57+
const ids = () => activeList().map((item) => item.id)
11858

11959
const [scrollRef, setScrollRef] = createSignal<HTMLElement | null>(null)
12060

@@ -138,10 +78,7 @@ export function Home(props: { softwareKeyboardShown: boolean }) {
13878
return (
13979
<>
14080
<div style={props.softwareKeyboardShown ? { display: "none" } : {}}>
141-
<ConnectionWarning
142-
isAuthenticated={connectionStatus().authenticated}
143-
isConnected={connectionStatus().connected}
144-
/>
81+
<ConnectionWarning isConnected={isConnected()} />
14582
</div>
14683

14784
<div ref={setScrollRef} class="text-lg flex-1 overflow-auto">
@@ -151,7 +88,7 @@ export function Home(props: { softwareKeyboardShown: boolean }) {
15188
<ul class="flex flex-col">
15289
<SortableProvider ids={ids()}>
15390
<RowAnimator>
154-
<For each={activeList()}>{(item) => <ItemRow item={item} actions={actions} />}</For>
91+
<For each={activeList()}>{(item) => <ItemRow item={item} />}</For>
15592
</RowAnimator>
15693
</SortableProvider>
15794
</ul>
@@ -187,7 +124,7 @@ export function Home(props: { softwareKeyboardShown: boolean }) {
187124
</h2>
188125
</button>
189126

190-
<button type="button" class="px-3 py-1" onClick={() => void client.clearCheckedItems()}>
127+
<button type="button" class="px-3 py-1" onClick={() => shopping.clearCheckedItems()}>
191128
Clear all
192129
</button>
193130
</div>
@@ -201,7 +138,7 @@ export function Home(props: { softwareKeyboardShown: boolean }) {
201138
exit={{ opacity: 0, transition: { duration: 0.2 } }}
202139
>
203140
<RowAnimator>
204-
<For each={checkedList()}>{(item) => <ItemRow item={item} actions={actions} />}</For>
141+
<For each={checkedList()}>{(item) => <ItemRow item={item} />}</For>
205142
</RowAnimator>
206143
</Motion.ul>
207144
</Show>
@@ -211,7 +148,7 @@ export function Home(props: { softwareKeyboardShown: boolean }) {
211148
</Presence>
212149
</div>
213150

214-
<NewItem onCreate={(name) => void client.addItem(name)} />
151+
<NewItem onCreate={(name) => shopping.addItem(name)} />
215152
</>
216153
)
217154
}

packages/app/src/components/ItemRow.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import { createSortable, transformStyle, useDragDropContext } from "@thisbeyond/solid-dnd"
22
import type { ShoppingListItem } from "lib"
33
import { Show, batch, createSignal } from "solid-js"
4+
import { shopping } from "~/lib/shopping-list"
45
import IconPadding from "~icons/ci/drag-vertical"
56
import IconTrash from "~icons/ci/trash-full"
67
import { Button } from "./Button"
78

8-
interface Actions {
9-
deleteItem: (id: string) => void
10-
setChecked: (id: string, checked: boolean) => void
11-
renameItem: (id: string, name: string) => void
12-
}
13-
149
const [focusingSomeRow, setFocusingSomeRow] = createSignal(false)
1510

16-
export function ItemRow(props: { item: ShoppingListItem; actions: Actions }) {
11+
export function ItemRow(props: { item: ShoppingListItem }) {
1712
const [hoveringThisRow, setHoveringThisRow] = createSignal(false)
1813
const [focusingThisRow, setFocusingThisRow] = createSignal(false)
1914

@@ -22,7 +17,7 @@ export function ItemRow(props: { item: ShoppingListItem; actions: Actions }) {
2217
const nameHasChanged = () => newName() !== props.item.name
2318

2419
function submitNameChange() {
25-
if (nameHasChanged()) props.actions.renameItem(props.item.id, newName())
20+
if (nameHasChanged()) shopping.renameItem(props.item.id, newName())
2621
}
2722

2823
let nameInputField: HTMLInputElement | undefined
@@ -81,7 +76,7 @@ export function ItemRow(props: { item: ShoppingListItem; actions: Actions }) {
8176
type="checkbox"
8277
checked={props.item.checked}
8378
onChange={(event) => {
84-
props.actions.setChecked(props.item.id, event.currentTarget.checked)
79+
shopping.setItemChecked(props.item.id, event.currentTarget.checked)
8580
}}
8681
/>
8782
</label>
@@ -110,7 +105,7 @@ export function ItemRow(props: { item: ShoppingListItem; actions: Actions }) {
110105

111106
<Show when={showingActions()}>
112107
<div class="flex items-center">
113-
<Button onClick={() => props.actions.deleteItem(props.item.id)}>
108+
<Button onClick={() => shopping.removeItem(props.item.id)}>
114109
<IconTrash height="100%" />
115110
</Button>
116111
</div>

packages/app/src/entry-server.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default createHandler(() => (
1313
<meta name="description" content="Your shopping list" />
1414

1515
<link rel="icon" type="image/png" href="/favicon.png" />
16-
<link rel="preload" href="Hubot-Sans-1.0.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
16+
<link rel="preload" href="/_build/Hubot-Sans-1.0.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
1717

1818
<Show when={import.meta.env.PROD}>
1919
<link rel="manifest" type="application/manifest+json" href="/manifest.webmanifest" />

packages/app/src/lib/browser-server-connection.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

packages/app/src/lib/env.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { z } from "zod"
22

33
export const env = z
44
.object({
5-
BACKEND_URL: z.string().url(),
65
WS_URL: z.string().url(),
6+
ENV_DISCRIMONATOR: z.string().default("dev"),
77
})
88
.parse({
9-
BACKEND_URL: import.meta.env.VITE_BACKEND_URL,
109
WS_URL: import.meta.env.VITE_WS_URL,
10+
ENV_DISCRIMONATOR: import.meta.env.VITE_ENV_DISCRIMONATOR,
1111
})

0 commit comments

Comments
 (0)