用纯 Bash 消费 Electric Shape Log:基于 `examples/bash` 的零依赖实时同步客户端实战 📅 发布时间:2026/9/15 22:46:54 👁 浏览次数: 用纯 Bash 消费 Electric Shape Log基于examples/bash的零依赖实时同步客户端实战【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric本文以仓库中的 examples/bash/README.md 与配套脚本 examples/bash/client.bash 为主线完整讲解如何在没有任何编程语言运行时无需 Node、无需 Python的情况下仅靠bash、curl、jq三个工具实现一个可用的 Electric HTTP API 客户端拉取 Shape 初始快照、识别up-to-date/must-refetch控制消息、切换到长轮询 live 模式并实时消费变更。读完本文你不仅能直接运行这个脚本还能理解 Electric Shape Log 协议的核心细节并把同样的思路迁移到任何“能发 HTTP、能解析 JSON”的环境中。一、背景为什么需要“Bash 版”客户端Electric 是一个构建在 Postgres 逻辑复制之上的同步引擎核心同步协议是低层 HTTP APIGET /v1/shape官方主推的是 TypeScript 客户端 与 Elixir 客户端 这类具备完整物化materialise能力、订阅subscribe机制的“重”客户端。但协议本身是“任何会说 HTTP 和 JSON 的语言都能消费”的——官方文档专门写了一篇 Client development 指南 来阐述这个模式。examples/bash正是这一理念的极端体现它不依赖任何语言运行时只要求系统里有bash、curl和jq三个命令。适用场景很明确运维/集成脚本在 CI/CD、容器启动脚本、cron 任务里消费同步数据极简环境只有 POSIX 工具链的服务器、容器或嵌入式环境协议学习用最直白的代码观察 Shape Log 的报文结构、控制消息和长轮询交互快速验证手动调试一个 shape URL看 Electric 返回什么。它的定位非常轻——README 的定位是“connect to any Electric shape URL and stream updates in real-time”。它只负责“消费并打印”不做数据物化不把日志应用成内存状态打印出的 JSON 流可供下游管道继续处理。二、环境要求与快速开始2.1 依赖清单工具用途验证方式bash脚本解释器含sed/grep/cut/tr等 POSIX 工具bash --versioncurl发起 HTTP 请求-i输出响应头用于读取electric-handle、electric-offset等头curl --versionjqJSON 解析美化打印、逐条遍历报文、读取headers.controljq --version三个缺一不可curl负责传输jq负责从 JSON 数组中过滤出控制消息up-to-date/must-refetchbash负责把两者串成有状态的循环。2.2 运行方式脚本接收且仅接收一个参数Shape URL。# 赋予可执行权限或直接用 bash 调用 chmod x client.bash # 消费整个 notes 表的 Shape Log ./client.bash http://localhost:3000/v1/shape?tablenotes # 也可以指定 where 参数做部分同步partial replication ./client.bash http://localhost:3000/v1/shape?tablenoteswheretitle LIKE $1params[1]foo%运行时脚本会输出两类信息下载到的 Shape Log JSON每批一次经jq .美化以及写入 stderr 的控制消息提示如Found control message、Shape is up to date, switching to live mode。stdout/stderr 分离的设计让 JSON 流可以被管道继续消费例如./client.bash ... 2/dev/null | jq -c .value。2.3 如何先跑起来一个 Electric 后端在仓库根目录用示例专用 Docker Compose 即可拉起 Postgres Electriccanary 镜像.support/docker-compose.yml 的关键配置如下services: postgres: image: postgres:16-alpine ports: [54321:5432] volumes: [./postgres.conf:/etc/postgresql/postgresql.conf:ro] backend: image: electricsql/electric:canary environment: DATABASE_URL: postgresql://postgres:passwordpostgres:5432/electric?sslmodedisable ELECTRIC_INSECURE: true # 仅开发环境使用 ports: [3000:3000]examples/bash的 package.json 里也封装了生命周期脚本# 启动后端拉镜像、启动容器并应用 db 迁移 pnpm backend:up # 停止并清理 pnpm backend:down这两个脚本实际委托给仓库根 package.json 中的example-backend:up/example-backend:down。启动后http://localhost:3000/v1/shape?tablenotes即为可用。README 给出的示例 URL 正是./client.bash http://localhost:3000/v1/shape?tablenotes。需要注意.support/docker-compose.yml中明确标注ELECTRIC_INSECURE: true不适合生产生产环境应通过反向代理做鉴权参见 HTTP API 文档 中的生产建议。三、首轮连接初始快照长什么样首次运行脚本你会看到一条包含整张表当前数据的 JSON 数组。README 中的示例输出[ { key: \public\.\notes\/\1\, value: { id: 1, title: Example Note, created_at: 2024-12-05 01:43:05.21995700 }, headers: { operation: insert, relation: [public, notes] }, offset: 0_0 } ]逐字段拆解这份 Shape Log 报文与 HTTP API 文档 的 Shape Log 定义一致key行的稳定标识格式为schema.table/主键值。客户端物化数据时可以用它作为 Map 的 key——Client development 指南 中的物化示例正是data.set(message.key, message.value)value行的完整当前值。值为字符串格式遵循 Postgres 的 display settings如时间戳2024-12-05 01:43:05.21995700headers.operation逻辑操作类型。初始快照阶段Electric 把查询结果批量转换成insert操作写进日志实时阶段会出现update和deleteheaders.relation来源表的三段式[schema, table]标识offset该报文在 Shape Log 中的位置形如0_0。客户端要把这个值作为下一次请求的?offset参数从而分页推进。注意数组末尾很可能还带有一个{ headers: { control: up-to-date } }这样的控制消息当报文恰好只有数据条目时则通过electric-up-to-date响应头表达。控制消息没有key/value只携带headers.control。四、脚本的协议实现细节README 只给了运行示例真正的协议逻辑藏在 client.bash 里。把它拆开看恰好对应 HTTP API 文档 描述的消费算法。4.1 URL 预处理剥离 Electric 专用参数BASE_URL$(echo $1 | sed -E s/\?.*//) # 去掉查询串拿到基础地址 QUERY_STRING$(echo $1 | sed -E -n s/.*\?(.*)/\1/p) # 单独取出查询参数 # 逐个参数遍历剔除 electric 专用参数保留 table / where 等形状定义参数 case $KEY in offset|handle|live) continue ;; *) # 保留并重组 esac这样设计的原因是调用者传入的 Shape URL 里只应有形状定义参数如table、where、columns而offset、handle、live是协议运行期参数必须由脚本根据每次响应的头信息动态生成。脚本通过cut -d -f1取参数名并跳过这三个保留字再重新拼接出形如{BASE}?tablenotes的模板供主循环追加运行期参数。4.2 响应解析用curl -i同时拿到响应头与报文体# 下载完整响应含响应头 curl -i -s $url $response_file # 以 [ 为分界拆成 body 与 headers 两部分 sed -n /^\[/,$p $response_file $tmp_body grep -B 1000 ^\[ $response_file | grep -v ^\[ $tmp_headers # 美化打印 JSON非空时 if [ -s $tmp_body ]; then jq . $tmp_body fi # 提取关键响应头 new_handle$(get_header_value $headers electric-handle) new_offset$(get_header_value $headers electric-offset)这里的electric-handle与electric-offset是两个核心响应头Client development 指南 中也有明确说明electric-handleElectric 为该 Shape Log 分配的临时标识ephemeral identifier客户端需要在后续请求中回传handleelectric-offset本次响应的最新日志位置下一次请求用offset继续往后拉。脚本的策略是“只要头里有就更新”——SHAPE_HANDLE和LATEST_OFFSET在每次响应后都同步为最新的值从而天然支持分页当一份快照太大、一次响应装不下时Electric 会返回一批数据加一个electric-offset客户端拿新 offset 继续请求直到收到up-to-date为止这正是 HTTP API 文档 描述的初始同步分页流程。4.3 控制消息识别up-to-date与must-refetch脚本只处理 JSON 数组的最后 5 条jq -c if length 5 then .[-5:] else . end | .[]因为控制消息总是出现在批次末尾。识别与响应逻辑if echo $item | jq -e .headers.control /dev/null 21; then control$(echo $item | jq -r .headers.control) 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_MODEfalse SHAPE_HANDLE ;; esac fi两种控制消息的含义与 HTTP API 文档 的 Control messages 一节逐字对应up-to-date客户端已拿到服务器在本次请求时刻所知的全部数据可以切入 live 模式。脚本据此把IS_LIVE_MODE置为truemust-refetch服务器要求客户端丢弃本地全部数据、从零重同步。例如形状定义被变更、底层表结构变化等情况会触发。脚本的处理是重置LATEST_OFFSET-1、清空 handle、退出 live 模式然后主循环自然带着offset-1重新发起完整同步。协议层面还有第三种snapshot-end控制消息用于 subset snapshot 请求携带xmin/xmax/xip_list等快照元数据普通同步中不使用脚本也未涉及。4.4 主循环长轮询 live 模式while true; do url${BASE_URL}offset$LATEST_OFFSET # 初始为 offset-1 if [ -n $SHAPE_HANDLE ]; then url${url}handle$SHAPE_HANDLE fi if [ $IS_LIVE_MODE true ]; then url${url}livetrue # 进入长轮询 fi if ! process_json $url; then echo 2 Error processing response, retrying in 5 seconds... sleep 5 continue fi sleep 1 # 请求间隔 done关键点初始请求offset-1语义是“从日志开头消费全部数据”HTTP API 文档 的 Initial sync request 一节live 模式收到up-to-date后脚本在 URL 上追加livetrue。此时 Electric 会保持连接打开直到超时默认约 20 秒或新数据到达。超时返回后脚本立刻用最新 offset 重连数据到达则立即把新报文推给客户端——这就是标准的长轮询long polling实时消费策略容错process_json失败时休眠 5 秒重试避免网络抖动直接杀掉脚本正常轮询之间也加 1 秒间隔防止对服务端造成无谓压力。一个值得注意的实现取舍脚本没有使用electric-cursor头与cursor参数这是 Client development 指南 提到的可选 cache-busting 参数用于在 CDN 上归一化 request-collapsing 行为。对直连本地 Electric 的脚本而言没有 CDN 参与跳过它完全不影响正确性。五、实时输出示例与解读README 给出了从初始快照切换到 live 模式的典型输出Found control message Control value: up-to-date Shape is up to date, switching to live mode这三行均写入 stderr对应脚本第 4.3 节的up-to-date分支。随后只要有人对notes表执行INSERT/UPDATE/DELETE脚本就会在下一轮长轮询中立刻收到对应的操作报文打印形如{ key: \public\.\notes\/\42\, value: { id: 42, title: New Note }, headers: { operation: insert, relation: [public, notes] }, offset: 0_12 }offset的推进0_0→0_12直观展示了 Shape Log 的追加式结构每条逻辑操作都有唯一的日志位置客户端正是靠它做到断点续传与去重。六、从 Bash 到“任意语言”这个例子的协议价值examples/bash的价值不在于 Bash 本身而在于它是 HTTP API 消费算法的最小可读实现。把脚本的逻辑抽象出来就是官方 Client development 指南 描述的通用流程构造 Shape URLGET /v1/shape携带table以及可选的where、columns等形状定义参数初始同步以offset-1发起请求逐批拉取每批用响应头的electric-handle/electric-offset更新请求参数直到出现up-to-date控制消息或electric-up-to-date响应头进入 live 模式追加livetrue以及handle、offset执行长轮询超时或新数据到达后立刻重连物化可选把insert/update/delete操作应用到本地数据结构。指南给出了 JSMap的物化伪代码Bash 脚本选择直接流式打印属于“只做第 13 步”的最简形态处理must-refetch丢弃本地数据回到第 2 步。仓库里还有两个更丰富的参照实现官方 packages/typescript-client含ShapeStream/Shape类与订阅机制和 packages/elixir-clientstream/3函数。对比阅读可以清晰看到Bash 版是协议骨架TypeScript/Elixir 版是在骨架上补齐了错误处理、退避重连、物化与响应式绑定。七、已知边界与注意事项无物化脚本只打印原始 Shape Log不维护“当前状态”。需要状态的应用应把key作为主键、value作为最新值自行累积并注意update报文中的value是增量字段而非整行无electric-cursor直连场景无影响如果将来把脚本接到 CDN 前面需要补充 cursor 支持以正确做请求合并must-refetch后的暴风重拉脚本收到must-refetch后立刻以offset-1重同步若服务器持续要求 refetch 可能形成循环请求生产环境建议加入退避头文件解析依赖响应格式脚本用grep -B 1000 ^\[按首行为[切分响应头与体这依赖 Electric 的 JSON 数组响应格式切换 SSElive_ssetrue等其它响应格式时不适用生产安全直接暴露的 Electric API 无鉴权示例环境用ELECTRIC_INSECURE: true。生产环境应将 Electric 置于自有后端 API 之后由代理完成鉴权与授权这与 HTTP API 文档 的 Production Best Practice 建议一致。八、小结本文以 examples/bash/README.md 为骨架、以 examples/bash/client.bash 为源码证据还原了一个完整的 Electric HTTP API 客户端实现从offset-1的初始快照到electric-handle/electric-offset驱动的日志分页再到up-to-date控制消息触发 live 长轮询、must-refetch触发全量重拉。整个客户端只有约 160 行 Bash依赖仅有bash/curl/jq却能完整跑通 Electric 同步协议的主干流程。无论你打算照抄这个脚本用于运维集成还是把它当作“用任意语言自研 Electric 客户端”的协议参考它都是最直观的起点。【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考