frontend client for gemstone. decentralised workplace app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: use zod

serenity effbf7dd 8e7259fd

+119 -53
+22 -11
app/hooks/useWebSocket.ts
··· 1 + import type { ShardMessage } from "@/app/lib/types/messages"; 2 + import { 3 + validateHistoryMessage, 4 + validateNewMessage, 5 + validateWsMessageString, 6 + validateWsMessageType, 7 + } from "@/app/lib/validators"; 1 8 import { useEffect, useRef, useState } from "react"; 2 9 3 10 export function useWebSocket(url: string) { 4 - const [messages, setMessages] = useState< 5 - { text: string; timestamp: string }[] 6 - >([]); 11 + const [messages, setMessages] = useState<ShardMessage[]>([]); 7 12 const [isConnected, setIsConnected] = useState(false); 8 13 const ws = useRef<WebSocket | null>(null); 9 14 ··· 17 22 }; 18 23 19 24 ws.current.onmessage = (event) => { 20 - const data = JSON.parse(event.data); 25 + const eventData = validateWsMessageString(event.data); 26 + if (!eventData) return; 27 + 28 + const data: unknown = JSON.parse(eventData); 29 + const wsMessage = validateWsMessageType(data); 30 + if (!wsMessage) return; 21 31 22 - if (data.type === "shard/history") { 23 - setMessages(data.messages); 24 - } else if (data.type === "shard/message") { 25 - setMessages((prev) => [ 26 - ...prev, 27 - { text: data.text, timestamp: data.timestamp }, 28 - ]); 32 + if (wsMessage.type === "shard/history") { 33 + const history = validateHistoryMessage(wsMessage); 34 + if (!history) return; 35 + if (history.messages) setMessages(history.messages); 36 + } else { 37 + const message = validateNewMessage(wsMessage); 38 + if (!message) return; 39 + setMessages((prev) => [...prev, message]); 29 40 } 30 41 }; 31 42
+1 -2
app/index.tsx
··· 1 1 import ChatComponent from "@/app/components/ChatComponent"; 2 - import { Text, View } from "react-native"; 2 + import { View } from "react-native"; 3 3 4 4 export default function Index() { 5 5 return ( ··· 10 10 alignItems: "center", 11 11 }} 12 12 > 13 - <Text>Edit app/index.tsx to edit this screen.</Text> 14 13 <ChatComponent /> 15 14 </View> 16 15 );
+22
app/lib/types/messages.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const websocketMessageSchema = z.object({ 4 + type: z.union([z.literal("shard/message"), z.literal("shard/history")]), 5 + }); 6 + 7 + export type WebsocketMessage = z.infer<typeof websocketMessageSchema>; 8 + 9 + export const shardMessageSchema = websocketMessageSchema.extend({ 10 + type: z.literal("shard/message"), 11 + text: z.string(), 12 + timestamp: z.coerce.date(), 13 + }); 14 + 15 + export type ShardMessage = z.infer<typeof shardMessageSchema>; 16 + 17 + export const historyMessageSchema = websocketMessageSchema.extend({ 18 + type: z.literal("shard/history"), 19 + messages: z.optional(z.array(shardMessageSchema)), 20 + }); 21 + 22 + export type HistoryMessage = z.infer<typeof historyMessageSchema>;
-39
app/lib/validator.ts
··· 1 - export function assertShardMessage( 2 - json: unknown, 3 - ): asserts json is ShardMessage { 4 - if (typeof json !== "object") { 5 - throw new Error("not a js object"); 6 - } 7 - 8 - const candidate = json as Record<string, unknown>; 9 - 10 - if (candidate.type !== "shard/message") { 11 - throw new Error("Invalid type"); 12 - } 13 - 14 - if (typeof candidate.text !== "string") { 15 - throw new Error("Invalid text"); 16 - } 17 - 18 - const timestamp = new Date(candidate.timestamp as string); 19 - 20 - if (!(timestamp instanceof Date)) { 21 - throw new Error("Invalid timestamp"); 22 - } 23 - } 24 - 25 - // example. we will use zod in the future. 26 - export const validateShardMessage = (json: unknown) => { 27 - try { 28 - assertShardMessage(json); 29 - return { success: true, data: json }; 30 - } catch (e: unknown) { 31 - return { error: e }; 32 - } 33 - }; 34 - 35 - export interface ShardMessage { 36 - type: "shard/message"; 37 - text: string; 38 - timestamp: Date; 39 - }
+64
app/lib/validators.ts
··· 1 + import { 2 + historyMessageSchema, 3 + shardMessageSchema, 4 + websocketMessageSchema, 5 + } from "@/app/lib/types/messages"; 6 + import { z } from "zod"; 7 + 8 + export const validateWsMessageString = (data: unknown) => { 9 + const { success, error, data: message } = z.string().safeParse(data); 10 + if (!success) { 11 + console.error("Error decoding websocket message"); 12 + console.error(error); 13 + return; 14 + } 15 + return message; 16 + }; 17 + 18 + export const validateWsMessageType = (data: unknown) => { 19 + const { 20 + success: wsMessageSuccess, 21 + error: wsMessageError, 22 + data: wsMessage, 23 + } = websocketMessageSchema.loose().safeParse(data); 24 + if (!wsMessageSuccess) { 25 + console.error( 26 + "Error parsing websocket message. The data might be the wrong shape.", 27 + ); 28 + console.error(wsMessageError); 29 + return; 30 + } 31 + return wsMessage; 32 + }; 33 + 34 + export const validateHistoryMessage = (data: unknown) => { 35 + const { 36 + success: historySuccess, 37 + error: historyError, 38 + data: history, 39 + } = historyMessageSchema.safeParse(data); 40 + if (!historySuccess) { 41 + console.error( 42 + "History message schema parsing failed. Did your type drift?", 43 + ); 44 + console.error(historyError); 45 + return; 46 + } 47 + return history; 48 + }; 49 + 50 + export const validateNewMessage = (data: unknown) => { 51 + const { 52 + success: messageSuccess, 53 + error: messageError, 54 + data: message, 55 + } = shardMessageSchema.safeParse(data); 56 + if (!messageSuccess) { 57 + console.error( 58 + "New message schema parsing failed. Did your type drift?", 59 + ); 60 + console.error(messageError); 61 + return; 62 + } 63 + return message; 64 + };
+2 -1
package.json
··· 40 40 "react-native-safe-area-context": "~5.6.0", 41 41 "react-native-screens": "~4.16.0", 42 42 "react-native-web": "~0.21.0", 43 - "react-native-worklets": "0.5.1" 43 + "react-native-worklets": "0.5.1", 44 + "zod": "^4.1.12" 44 45 }, 45 46 "devDependencies": { 46 47 "@types/react": "~19.1.0",
+8
pnpm-lock.yaml
··· 83 83 react-native-worklets: 84 84 specifier: 0.5.1 85 85 version: 0.5.1(@babel/core@7.28.4)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 86 + zod: 87 + specifier: ^4.1.12 88 + version: 4.1.12 86 89 devDependencies: 87 90 '@types/react': 88 91 specifier: ~19.1.0 ··· 4339 4342 4340 4343 zod@3.25.76: 4341 4344 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 4345 + 4346 + zod@4.1.12: 4347 + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} 4342 4348 4343 4349 snapshots: 4344 4350 ··· 9507 9513 zod: 3.25.76 9508 9514 9509 9515 zod@3.25.76: {} 9516 + 9517 + zod@4.1.12: {}