-
Notifications
You must be signed in to change notification settings - Fork 174
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix bash example client to work with latest server (#2208)
- Loading branch information
1 parent
57eafd8
commit 7e6f6c4
Showing
3 changed files
with
183 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,4 +21,5 @@ tsconfig.tsbuildinfo | |
wal | ||
/shapes | ||
.sst | ||
**/deps/* | ||
**/deps/* | ||
response.tmp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Electric Bash Client | ||
|
||
A simple bash client for consuming Electric shape logs. This client can connect to any Electric shape URL and stream updates in real-time. | ||
|
||
## Requirements | ||
|
||
- bash | ||
- curl | ||
- jq (for JSON processing) | ||
|
||
## Usage | ||
|
||
```bash | ||
./client.bash 'YOUR_SHAPE_URL' | ||
``` | ||
|
||
For example: | ||
```bash | ||
./client.bash 'http://localhost:3000/v1/shape?table=notes' | ||
``` | ||
|
||
## Example Output | ||
|
||
When first connecting, you'll see the initial shape data: | ||
```json | ||
[ | ||
{ | ||
"key": "\"public\".\"notes\"/\"1\"", | ||
"value": { | ||
"id": "1", | ||
"title": "Example Note", | ||
"created_at": "2024-12-05 01:43:05.219957+00" | ||
}, | ||
"headers": { | ||
"operation": "insert", | ||
"relation": [ | ||
"public", | ||
"notes" | ||
] | ||
}, | ||
"offset": "0_0" | ||
} | ||
] | ||
``` | ||
|
||
Once caught up, the client switches to live mode and streams updates: | ||
``` | ||
Found control message | ||
Control value: up-to-date | ||
Shape is up to date, switching to live mode | ||
``` | ||
|
||
Any changes to the shape will be streamed in real-time. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,88 +1,161 @@ | ||
#!/bin/bash | ||
|
||
# URL to download the JSON file from (without the output parameter) | ||
BASE_URL="http://localhost:3000/v1/shape?table=todos" | ||
if [ "$#" -ne 1 ]; then | ||
echo "Usage: $0 <shape-url>" | ||
echo "Example: $0 'http://localhost:3000/v1/shape?table=todos'" | ||
exit 1 | ||
fi | ||
|
||
# Extract base URL (everything before the query string) | ||
BASE_URL=$(echo "$1" | sed -E 's/\?.*//') | ||
# Extract and clean query parameters | ||
QUERY_STRING=$(echo "$1" | sed -n 's/.*\?\(.*\)/\1/p') | ||
|
||
# Build cleaned query string, removing electric-specific params but keeping others | ||
if [ -n "$QUERY_STRING" ]; then | ||
# Split query string into individual parameters | ||
CLEANED_PARAMS="" | ||
IFS='&' read -ra PARAMS <<< "$QUERY_STRING" | ||
for param in "${PARAMS[@]}"; do | ||
KEY=$(echo "$param" | cut -d'=' -f1) | ||
# Skip electric-specific params | ||
case "$KEY" in | ||
"offset"|"handle"|"live") continue ;; | ||
*) | ||
if [ -z "$CLEANED_PARAMS" ]; then | ||
CLEANED_PARAMS="$param" | ||
else | ||
CLEANED_PARAMS="${CLEANED_PARAMS}&${param}" | ||
fi | ||
;; | ||
esac | ||
done | ||
|
||
# Add question mark if we have params | ||
if [ -n "$CLEANED_PARAMS" ]; then | ||
BASE_URL="${BASE_URL}?${CLEANED_PARAMS}&" | ||
else | ||
BASE_URL="${BASE_URL}?" | ||
fi | ||
else | ||
BASE_URL="${BASE_URL}?" | ||
fi | ||
|
||
# Directory to store individual JSON files | ||
OFFSET_DIR="./json_files" | ||
|
||
# Initialize the latest output variable | ||
# Initialize variables | ||
LATEST_OFFSET="-1" | ||
SHAPE_HANDLE="" | ||
IS_LIVE_MODE=false | ||
|
||
# Create the output directory if it doesn't exist | ||
mkdir -p "$OFFSET_DIR" | ||
|
||
# Function to extract header value from curl response | ||
get_header_value() { | ||
local headers="$1" | ||
local header_name="$2" | ||
echo "$headers" | grep -i "^$header_name:" | cut -d':' -f2- | tr -d ' \r' | ||
} | ||
|
||
# Function to download and process JSON data | ||
process_json() { | ||
local url="$1" | ||
local output_file="$2" | ||
|
||
echo >&2 "Downloading JSON file from $url..." | ||
curl -s -o "$output_file" "$url" | ||
|
||
# Check if the file was downloaded successfully | ||
if [ ! -f "$output_file" ]; then | ||
echo >&2 "Failed to download the JSON file." | ||
exit 1 | ||
local tmp_headers="headers.tmp" | ||
local tmp_body="body.tmp" | ||
local response_file="response.tmp" | ||
local state_file="state.tmp" | ||
|
||
echo "Downloading shape log from URL: ${url}" | ||
|
||
# Clear any existing tmp files and create new ones | ||
rm -f "$tmp_headers" "$tmp_body" "$response_file" "$state_file" xx* | ||
|
||
# Download the entire response first | ||
curl -i -s "$url" > "$response_file" | ||
|
||
# Split at the double newline - everything before the JSON array | ||
sed -n '/^\[/,$p' "$response_file" > "$tmp_body" | ||
grep -B 1000 "^\[" "$response_file" | grep -v "^\[" > "$tmp_headers" | ||
|
||
# Display prettified JSON if file is not empty | ||
if [ -s "$tmp_body" ]; then | ||
jq '.' < "$tmp_body" | ||
fi | ||
|
||
# Check if the file is not empty | ||
if [ ! -s "$output_file" ]; then | ||
echo >&2 "The downloaded JSON file is empty." | ||
# Return the latest OFFSET | ||
echo "$LATEST_OFFSET" | ||
return | ||
fi | ||
# Extract important headers | ||
local headers=$(cat "$tmp_headers") | ||
local new_handle=$(get_header_value "$headers" "electric-handle") | ||
local new_offset=$(get_header_value "$headers" "electric-offset") | ||
|
||
echo >&2 "Successfully downloaded the JSON file." | ||
# Always update handle from header if present | ||
if [ -n "$new_handle" ]; then | ||
SHAPE_HANDLE="$new_handle" | ||
fi | ||
|
||
# Ensure the file ends with a newline | ||
if [ -n "$(tail -c 1 "$output_file")" ]; then | ||
echo >> "$output_file" | ||
# Update offset from header | ||
if [ -n "$new_offset" ]; then | ||
LATEST_OFFSET="$new_offset" | ||
fi | ||
|
||
# Validate the JSON structure (optional but recommended for debugging) | ||
if ! jq . "$output_file" > /dev/null 2>&1; then | ||
echo >&2 "Invalid JSON format in the downloaded file." | ||
exit 1 | ||
# Check if headers were received | ||
if [ ! -f "$tmp_headers" ]; then | ||
echo >&2 "Failed to download response." | ||
return 1 | ||
fi | ||
|
||
# Read the JSON file line by line and save each JSON object to an individual file | ||
while IFS= read -r line; do | ||
# Check if the headers array contains an object with key "operation" | ||
if echo "$line" | jq -e '.headers | map(select(.key == "operation")) | length == 0' > /dev/null; then | ||
# echo "Skipping line without an operation: $operation" # Log skipping non-data objects | ||
continue | ||
# Process last 5 items in the JSON array for control messages | ||
jq -c 'if length > 5 then .[-5:] else . end | .[]' "$tmp_body" | while IFS= read -r item; do | ||
# Parse the JSON message | ||
if echo "$item" | jq -e '.headers.control' >/dev/null 2>&1; then | ||
echo "Found control message" >&2 | ||
# Handle control messages | ||
local control=$(echo "$item" | jq -r '.headers.control') | ||
echo "Control value: $control" >&2 | ||
case "$control" in | ||
"up-to-date") | ||
echo "true" > "$state_file" | ||
echo >&2 "Shape is up to date, switching to live mode" | ||
;; | ||
"must-refetch") | ||
echo >&2 "Server requested refetch" | ||
LATEST_OFFSET="-1" | ||
IS_LIVE_MODE=false | ||
SHAPE_HANDLE="" | ||
;; | ||
esac | ||
fi | ||
done | ||
|
||
key=$(echo "$line" | jq -r '.key') | ||
offset=$(echo "$line" | jq -r '.offset') | ||
|
||
if [ -z "$key" ]; then | ||
echo >&2 "No key found in message: $line" # Log if no ID is found | ||
else | ||
echo >&2 "Extracted key: $key" # Log the extracted key | ||
echo "$line" | jq . > "$OFFSET_DIR/json_object_$key.json" | ||
echo >&2 "Written to file: $OFFSET_DIR/json_object_$key.json" # Log file creation | ||
|
||
LATEST_OFFSET="$offset" | ||
echo >&2 "Updated latest OFFSET to: $LATEST_OFFSET" | ||
fi | ||
done < <(jq -c '.[]' "$output_file") | ||
# Read the state file and update IS_LIVE_MODE | ||
if [ -f "$state_file" ] && [ "$(cat "$state_file")" = "true" ]; then | ||
IS_LIVE_MODE=true | ||
fi | ||
|
||
echo >&2 "done with jq/read loop $LATEST_OFFSET" | ||
# Cleanup | ||
rm -f "$tmp_headers" "$tmp_body" "$response_file" "$state_file" xx* | ||
|
||
# Return the latest OFFSET | ||
echo "$LATEST_OFFSET" | ||
return 0 | ||
} | ||
|
||
# Main loop to poll for updates every second | ||
# Main loop to poll for updates | ||
while true; do | ||
url="$BASE_URL&offset=$LATEST_OFFSET" | ||
echo $url | ||
# Construct URL with appropriate parameters | ||
url="${BASE_URL}offset=$LATEST_OFFSET" | ||
if [ -n "$SHAPE_HANDLE" ]; then | ||
url="${url}&handle=$SHAPE_HANDLE" | ||
fi | ||
if [ "$IS_LIVE_MODE" = true ]; then | ||
url="${url}&live=true" | ||
fi | ||
|
||
LATEST_OFFSET=$(process_json "$url" "shape-data.json") | ||
if ! process_json "$url"; then | ||
echo >&2 "Error processing response, retrying in 5 seconds..." | ||
sleep 5 | ||
continue | ||
fi | ||
|
||
# Add small delay between requests | ||
sleep 1 | ||
done | ||
|