-
+ <>
+
+
+ {isConnected ? (
+ <>
+
Connected to Sync Agent
+
+ >
+ ) : (
+
Not Connected
+ )}
+ >
+ )
+}
+
+const ConnectedContent = () => {
+ const { data: nodeUrls } = useSyncValue((s) => s.riverStreamNodeUrls, {
+ onUpdate: (data) => console.log('onUpdate', data),
+ onError: (error) => console.error('onError', error),
+ onSaved: (data) => console.log('onSaved', data),
+ })
+ return (
+
+
+ {JSON.stringify(nodeUrls, null, 2)}
+
)
}
diff --git a/packages/playground/src/utils/viem-to-ethers.ts b/packages/playground/src/utils/viem-to-ethers.ts
new file mode 100644
index 000000000..4f8117655
--- /dev/null
+++ b/packages/playground/src/utils/viem-to-ethers.ts
@@ -0,0 +1,24 @@
+import * as React from 'react'
+import { type WalletClient, useWalletClient } from 'wagmi'
+import { providers } from 'ethers'
+
+export function walletClientToSigner(walletClient: WalletClient) {
+ const { account, chain, transport } = walletClient
+ const network = {
+ chainId: chain.id,
+ name: chain.name,
+ ensAddress: chain.contracts?.ensRegistry?.address,
+ }
+ const provider = new providers.Web3Provider(transport, network)
+ const signer = provider.getSigner(account.address)
+ return signer
+}
+
+/** Hook to convert a viem Wallet Client to an ethers.js Signer. */
+export function useEthersSigner({ chainId }: { chainId?: number } = {}) {
+ const { data: walletClient } = useWalletClient({ chainId })
+ return React.useMemo(
+ () => (walletClient ? walletClientToSigner(walletClient) : undefined),
+ [walletClient],
+ )
+}
diff --git a/packages/react-sdk/README.md b/packages/react-sdk/README.md
index a3d9e1baf..25936c2f8 100644
--- a/packages/react-sdk/README.md
+++ b/packages/react-sdk/README.md
@@ -12,9 +12,55 @@ yarn add @river-build/react-sdk
# Usage
-TODO for now. But that's what we need to solve:
+## Connect to River
-## Connect to a River stream
+`@river-build/react-sdk` suggests you to use Wagmi to connect to River.
+Wrap your app with `RiverSyncProvider` and use the `useConnectRiver` hook to connect to River.
+
+> [!note] If you're using Viem
+> You'll need to use `useEthersSigner` to get the signer from viem wallet client.
+> You can get the hook from [wagmi docs](https://wagmi.sh/react/guides/ethers#usage-1).
+
+```tsx
+import {
+ RiverSyncProvider,
+ useConnectRiver,
+ useConnection,
+} from "@river-build/react-sdk";
+import { makeRiverConfig } from "@river-build/sdk";
+import { WagmiProvider } from "wagmi";
+import { useEthersSigner } from "./utils/viem-to-ethers";
+
+const riverConfig = makeRiverConfig("gamma");
+
+const App = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const ConnectRiver = () => {
+ const { connect, isConnecting, isConnected } = useConnectRiver();
+ const signer = useEthersSigner();
+
+ return (
+ <>
+
+ {isConnected &&
Connected!}
+ >
+ );
+};
+```
## Get information about an account
diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json
index 55abc1017..49817741b 100644
--- a/packages/react-sdk/package.json
+++ b/packages/react-sdk/package.json
@@ -11,6 +11,7 @@
"build": "yarn run clean && yarn run build:esm+types",
"build:esm+types": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
+ "watch": "yarn build -w",
"test:build": "publint --strict && attw --pack --ignore-rules cjs-resolves-to-esm",
"typecheck": "tsc --noEmit"
},
@@ -23,13 +24,17 @@
],
"sideEffects": false,
"type": "module",
- "main": "./dist/esm/exports/index.js",
- "types": "./dist/types/exports/index.d.ts",
- "typings": "./dist/types/exports/index.d.ts",
+ "main": "./dist/esm/index.js",
+ "types": "./dist/types/index.d.ts",
+ "typings": "./dist/types/index.d.ts",
"peerDependencies": {
"react": "^18.2.0",
"typescript": "^5.1.6"
},
+ "dependencies": {
+ "@river-build/sdk": "workspace:^",
+ "ethers": "^5.7.2"
+ },
"devDependencies": {
"@testing-library/react": "^14.2.1",
"@types/react": "^18.2.11",
diff --git a/packages/react-sdk/src/RiverSyncProvider.tsx b/packages/react-sdk/src/RiverSyncProvider.tsx
new file mode 100644
index 000000000..1ed75a918
--- /dev/null
+++ b/packages/react-sdk/src/RiverSyncProvider.tsx
@@ -0,0 +1,30 @@
+'use client'
+import type { SyncAgent } from '@river-build/sdk'
+import { useEffect, useState } from 'react'
+import { RiverSyncContext } from './internals/RiverSyncContext'
+
+type RiverSyncProviderProps = {
+ syncAgent?: SyncAgent
+ children?: React.ReactNode
+}
+
+export const RiverSyncProvider = (props: RiverSyncProviderProps) => {
+ const [syncAgent, setSyncAgent] = useState(() => props.syncAgent)
+
+ useEffect(() => {
+ if (syncAgent) {
+ syncAgent.start()
+ }
+ }, [syncAgent])
+
+ return (
+
+ {props.children}
+
+ )
+}
diff --git a/packages/react-sdk/src/connectRiver.ts b/packages/react-sdk/src/connectRiver.ts
new file mode 100644
index 000000000..40d4b6b2e
--- /dev/null
+++ b/packages/react-sdk/src/connectRiver.ts
@@ -0,0 +1,13 @@
+/// This file can be used on server side to create a River Client
+/// We don't want a 'use client' directive here
+import { SyncAgent, type SyncAgentConfig, makeSignerContext } from '@river-build/sdk'
+import { ethers } from 'ethers'
+
+export const connectRiver = async (
+ signer: ethers.Signer,
+ config: Omit
,
+): Promise => {
+ const delegateWallet = ethers.Wallet.createRandom()
+ const signerContext = await makeSignerContext(signer, delegateWallet)
+ return new SyncAgent({ context: signerContext, ...config })
+}
diff --git a/packages/react-sdk/src/context.ts b/packages/react-sdk/src/context.ts
deleted file mode 100644
index cd3a62059..000000000
--- a/packages/react-sdk/src/context.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-'use client'
-
-export const hello = () => 'world'
diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts
index 402e2ddac..4360e3096 100644
--- a/packages/react-sdk/src/index.ts
+++ b/packages/react-sdk/src/index.ts
@@ -1 +1,10 @@
-export { hello } from './context'
+/**************************************************************************
+ * This file can be auto generated by 🏕️ scripts/generate_sdk_index.sh 🏕️ *
+ **************************************************************************/
+export * from './RiverSyncProvider'
+export * from './connectRiver'
+export * from './useObservable'
+export * from './useRiverConnection'
+export * from './useSyncAgent'
+export * from './useSyncValue'
+export * from './utils'
diff --git a/packages/react-sdk/src/internals/RiverSyncContext.tsx b/packages/react-sdk/src/internals/RiverSyncContext.tsx
new file mode 100644
index 000000000..517998bd2
--- /dev/null
+++ b/packages/react-sdk/src/internals/RiverSyncContext.tsx
@@ -0,0 +1,9 @@
+'use client'
+import { SyncAgent } from '@river-build/sdk'
+import { createContext } from 'react'
+
+type RiverSyncContextType = {
+ syncAgent: SyncAgent | undefined
+ setSyncAgent: (syncAgent: SyncAgent | undefined) => void
+}
+export const RiverSyncContext = createContext(undefined)
diff --git a/packages/react-sdk/src/internals/useRiverSync.tsx b/packages/react-sdk/src/internals/useRiverSync.tsx
new file mode 100644
index 000000000..3f8f66efb
--- /dev/null
+++ b/packages/react-sdk/src/internals/useRiverSync.tsx
@@ -0,0 +1,4 @@
+'use client'
+import { useContext } from 'react'
+import { RiverSyncContext } from './RiverSyncContext'
+export const useRiverSync = () => useContext(RiverSyncContext)
diff --git a/packages/react-sdk/src/useObservable.ts b/packages/react-sdk/src/useObservable.ts
new file mode 100644
index 000000000..4ff08f11d
--- /dev/null
+++ b/packages/react-sdk/src/useObservable.ts
@@ -0,0 +1,112 @@
+'use client'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { type Observable, type PersistedModel } from '@river-build/sdk'
+
+export type ObservableConfig = {
+ fireImmediately?: boolean
+ onUpdate?: (data: T) => void
+ onError?: (error: Error) => void
+ onSaved?: (data: T) => void
+}
+
+type ObservableReturn = {
+ data: T | undefined
+ error: Error | undefined
+ status: PersistedModel['status']
+ isLoading: boolean
+ isError: boolean
+ isSaving: boolean
+ isSaved: boolean
+ isLoaded: boolean
+}
+
+// Needed to treat Observable and Observable> as the same
+const makeDataModel = (value: T): PersistedModel => ({
+ status: 'loaded',
+ data: value,
+})
+
+const isPersisted = (value: unknown): value is PersistedModel => {
+ if (typeof value !== 'object') {
+ return false
+ }
+ if (value === null) {
+ return false
+ }
+ return 'status' in value && 'data' in value
+}
+
+export function useObservable(
+ observable: Observable | undefined,
+ config?: ObservableConfig,
+): ObservableReturn {
+ const [value, setValue] = useState | undefined>(
+ observable?.value
+ ? isPersisted(observable.value)
+ ? observable?.value
+ : makeDataModel(observable.value)
+ : undefined,
+ )
+
+ const opts = { fireImmediately: true, ...config } satisfies ObservableConfig
+
+ const onSubscribe = useCallback(
+ (newValue: PersistedModel | T) => {
+ let value: PersistedModel | undefined
+ if (isPersisted(newValue)) {
+ value = newValue
+ } else {
+ value = makeDataModel(newValue)
+ }
+ setValue(value)
+ if (value.status === 'loaded') {
+ opts.onUpdate?.(value.data)
+ }
+ if (value.status === 'error') {
+ opts.onError?.(value.error)
+ }
+ if (value.status === 'saved') {
+ opts.onSaved?.(value.data)
+ }
+ },
+ [opts],
+ )
+
+ useEffect(() => {
+ if (!observable) {
+ return
+ }
+ const subscription = observable.subscribe(onSubscribe, {
+ fireImediately: opts?.fireImmediately,
+ })
+ return () => subscription.unsubscribe(onSubscribe)
+ }, [opts, observable, onSubscribe])
+
+ const data = useMemo(() => {
+ if (!value) {
+ return {
+ data: undefined,
+ error: undefined,
+ status: 'loading',
+ isLoading: true,
+ isError: false,
+ isSaving: false,
+ isLoaded: false,
+ isSaved: false,
+ }
+ }
+ const { data, status } = value
+ return {
+ data,
+ error: status === 'error' ? value.error : undefined,
+ status,
+ isLoading: status === 'loading',
+ isError: status === 'error',
+ isSaving: status === 'saving',
+ isLoaded: status === 'loaded',
+ isSaved: status === 'saved',
+ }
+ }, [value]) satisfies ObservableReturn
+
+ return data
+}
diff --git a/packages/react-sdk/src/useRiverConnection.ts b/packages/react-sdk/src/useRiverConnection.ts
new file mode 100644
index 000000000..47e672a5c
--- /dev/null
+++ b/packages/react-sdk/src/useRiverConnection.ts
@@ -0,0 +1,30 @@
+import type { SyncAgentConfig } from '@river-build/sdk'
+import { useCallback, useMemo, useState } from 'react'
+import type { ethers } from 'ethers'
+import { connectRiver } from './connectRiver'
+import { useRiverSync } from './internals/useRiverSync'
+
+export const useRiverConnection = () => {
+ const [isConnecting, setConnecting] = useState(false)
+ const river = useRiverSync()
+
+ const connect = useCallback(
+ async (signer: ethers.Signer, config: Omit) => {
+ if (river?.syncAgent) {
+ return
+ }
+
+ setConnecting(true)
+ return connectRiver(signer, config)
+ .then((syncAgent) => river?.setSyncAgent(syncAgent))
+ .finally(() => setConnecting(false))
+ },
+ [river],
+ )
+
+ const disconnect = useCallback(() => river?.setSyncAgent(undefined), [river])
+
+ const isConnected = useMemo(() => !!river?.syncAgent, [river])
+
+ return { connect, disconnect, isConnecting, isConnected }
+}
diff --git a/packages/react-sdk/src/useSyncAgent.tsx b/packages/react-sdk/src/useSyncAgent.tsx
new file mode 100644
index 000000000..9df21c92e
--- /dev/null
+++ b/packages/react-sdk/src/useSyncAgent.tsx
@@ -0,0 +1,15 @@
+'use client'
+import { useRiverSync } from './internals/useRiverSync'
+
+export const useSyncAgent = () => {
+ const river = useRiverSync()
+
+ if (!river?.syncAgent) {
+ console.error(
+ 'No SyncAgent set, use RiverSyncProvider to set one or use useConnected to check if connected',
+ )
+ return undefined
+ }
+
+ return river.syncAgent
+}
diff --git a/packages/react-sdk/src/useSyncValue.ts b/packages/react-sdk/src/useSyncValue.ts
new file mode 100644
index 000000000..51dc7f5b5
--- /dev/null
+++ b/packages/react-sdk/src/useSyncValue.ts
@@ -0,0 +1,15 @@
+'use client'
+import type { Observable, SyncAgent } from '@river-build/sdk'
+import { type ObservableConfig, useObservable } from './useObservable'
+import { useSyncAgent } from './useSyncAgent'
+
+type SyncLens = SyncAgent['observables']
+
+// TODO: maybe we should call this useRiver?
+export function useSyncValue(
+ fn: (sync: SyncLens) => Observable,
+ config?: ObservableConfig,
+) {
+ const syncAgent = useSyncAgent()
+ return useObservable(syncAgent ? fn(syncAgent.observables) : undefined, config)
+}
diff --git a/packages/react-sdk/src/utils.ts b/packages/react-sdk/src/utils.ts
new file mode 100644
index 000000000..00967d925
--- /dev/null
+++ b/packages/react-sdk/src/utils.ts
@@ -0,0 +1,8 @@
+import type { PersistedModel } from '@river-build/sdk'
+
+export const isPersistedModel = (data: T | PersistedModel): data is PersistedModel => {
+ if (typeof data === 'object' && data !== null) {
+ return 'status' in data
+ }
+ return false
+}
diff --git a/packages/react-sdk/tsconfig.build.json b/packages/react-sdk/tsconfig.build.json
index 676931c2d..4c6dfcf9e 100644
--- a/packages/react-sdk/tsconfig.build.json
+++ b/packages/react-sdk/tsconfig.build.json
@@ -1,5 +1,8 @@
{
"extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "preserve"
+ },
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.test-d.ts"]
}
diff --git a/packages/react-sdk/tsconfig.json b/packages/react-sdk/tsconfig.json
index 4cd15526b..178dc1178 100644
--- a/packages/react-sdk/tsconfig.json
+++ b/packages/react-sdk/tsconfig.json
@@ -1,8 +1,5 @@
{
"extends": "./tsconfig.build.json",
- "compilerOptions": {
- "jsx": "preserve"
- },
"include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx", ".eslintrc.cjs"],
"exclude": []
}
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index e9914c1f2..76c7c57b1 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -3,15 +3,22 @@
**************************************************************************/
export * from './check'
export * from './client'
+export * from './clientDecryptionExtensions'
export * from './encryptedContentTypes'
export * from './id'
-export * from './makeStreamRpcClient'
export * from './makeRiverRpcClient'
+export * from './makeStreamRpcClient'
+export * from './migrations/migrateSnapshot'
+export * from './migrations/snapshotMigration0000'
+export * from './migrations/snapshotMigration0001'
+export * from './observable/observable'
+export * from './observable/persistedObservable'
export * from './persistenceStore'
export * from './riverConfig'
export * from './riverDbManager'
export * from './sign'
export * from './signerContext'
+export * from './store/store'
export * from './stream'
export * from './streamEvents'
export * from './streamStateView'
@@ -32,6 +39,19 @@ export * from './streamStateView_UserInbox'
export * from './streamStateView_UserMetadata'
export * from './streamStateView_UserSettings'
export * from './streamUtils'
+export * from './sync-agent/entitlements/entitlements'
+export * from './sync-agent/river-connection/models/streamNodeUrls'
+export * from './sync-agent/river-connection/riverConnection'
+export * from './sync-agent/syncAgent'
+export * from './sync-agent/syncAgentStore'
+export * from './sync-agent/user/models/userDeviceKeys'
+export * from './sync-agent/user/models/userInbox'
+export * from './sync-agent/user/models/userMemberships'
+export * from './sync-agent/user/models/userSettings'
+export * from './sync-agent/user/user'
+export * from './sync-agent/utils/promiseQueue'
+export * from './sync-agent/utils/providers'
+export * from './sync-agent/utils/spaceUtils'
export * from './syncEvents'
export * from './syncedStream'
export * from './syncedStreams'
@@ -39,5 +59,7 @@ export * from './syncedStreamsExtension'
export * from './types'
export * from './unauthenticatedClient'
export * from './userMetadata_DisplayNames'
+export * from './userMetadata_EnsAddresses'
+export * from './userMetadata_Nft'
export * from './userMetadata_Usernames'
export * from './utils'
diff --git a/core/scripts/generate_sdk_index.sh b/scripts/generate_sdk_index.sh
similarity index 72%
rename from core/scripts/generate_sdk_index.sh
rename to scripts/generate_sdk_index.sh
index 3567fbbe2..e7608dfe4 100755
--- a/core/scripts/generate_sdk_index.sh
+++ b/scripts/generate_sdk_index.sh
@@ -5,7 +5,7 @@
cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")"
# Array of directories to process
-declare -a dirs=("../sdk/src")
+declare -a dirs=("../packages/sdk/src" "../packages/react-sdk/src")
# Loop through each directory
for dir in "${dirs[@]}"; do
@@ -23,10 +23,13 @@ for dir in "${dirs[@]}"; do
# Loop through each TypeScript file to append an export statement to the array
# Skip files that have ".test." in their name, the existing index.ts file, and directories
- for file in $(find . -type f -name "*.ts" ! -name "*.test*" ! -name "*.d.ts" ! -name "index.ts" | sort); do
- # Remove the './' prefix and '.ts' suffix from the file path
- file=$(echo "$file" | sed "s|^\./||;s|\.ts$||")
-
+ for file in $(find . -type f \( -name "*.ts" -o -name "*.tsx" \) ! -name "*.test*" ! -name "*.d.ts" ! -name "index.ts" ! -path "*/internals/*" | sort); do
+ # Remove the './' prefix and '.tsx' or '.ts' suffix from the file path
+ file_without_slash=${file#./}
+ file_without_tsx=${file_without_slash%.tsx}
+ file_without_ts=${file_without_tsx%.ts}
+ file=${file_without_ts}
+
# Append export statement to the array
exports+=("export * from './$file'")
done
@@ -38,4 +41,4 @@ for dir in "${dirs[@]}"; do
# Change back to the original directory to continue the loop
cd - || exit
-done
+done
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index de77884e5..05b928c2d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3537,6 +3537,8 @@ __metadata:
"@radix-ui/react-switch": ^1.0.3
"@river-build/eslint-config": "workspace:^"
"@river-build/prettier-config": "workspace:^"
+ "@river-build/react-sdk": "workspace:^"
+ "@river-build/sdk": "workspace:^"
"@types/node": ^20.5.0
"@types/react": ^18.2.11
"@types/react-dom": ^18.2.4
@@ -3550,6 +3552,7 @@ __metadata:
eslint-plugin-import: ^2.27.5
eslint-plugin-react: ^7.32.2
eslint-plugin-react-hooks: ^4.6.0
+ ethers: ^5.7.2
postcss: ^8.4.38
prettier: ^2.8.8
prettier-plugin-tailwindcss: ^0.4.1
@@ -3561,8 +3564,10 @@ __metadata:
tailwindcss: ^3.4.4
tailwindcss-animate: ^1.0.7
typescript: ^5.1.6
+ viem: ^1.18.2
vite: ^5.3.1
vite-plugin-checker: ^0.6.4
+ wagmi: ^1.4.12
zod: ^3.21.4
languageName: unknown
linkType: soft
@@ -3603,10 +3608,11 @@ __metadata:
languageName: unknown
linkType: soft
-"@river-build/react-sdk@workspace:packages/react-sdk":
+"@river-build/react-sdk@workspace:^, @river-build/react-sdk@workspace:packages/react-sdk":
version: 0.0.0-use.local
resolution: "@river-build/react-sdk@workspace:packages/react-sdk"
dependencies:
+ "@river-build/sdk": "workspace:^"
"@testing-library/react": ^14.2.1
"@types/react": ^18.2.11
"@types/react-dom": ^18.2.4
@@ -3616,6 +3622,7 @@ __metadata:
eslint-plugin-import: ^2.27.5
eslint-plugin-react: ^7.32.2
eslint-plugin-react-hooks: ^4.6.0
+ ethers: ^5.7.2
react: ^18.2.0
react-dom: ^18.2.0
typescript: ^5.1.6