服务端状态、账号与离线同步
连接查询缓存、session、OAuth、实时与离线队列。
本页整合 Electron 归档分稿中的机制、接口和接线。归档使用 Electron 44 系列作为其版本讨论背景;本文接口依据官方文档及 breaking changes 核查,实际采用时仍需固定项目版本。包版本、维护状态和原稿实测均是对应日期的历史信息。本次未启动 Electron、构建安装包、签名、公证或验证更新。代码块按各自标注区分片段与组合示例。
调研时间:2026-10。覆盖范围:服务端状态管理库在 Electron 中的落地、网络层与 session/cookie 处理、OAuth 桌面端鉴权、实时同步(WebSocket/SSE、多窗口广播、断线重连、乐观更新)、离线优先与同步引擎选型、桌面端与 Web 端状态架构的本质差异。所有事实性结论均标注来源;无法核实的内容明确标注「待核实」。
1. 原理与机制:桌面端与 Web 端状态架构的本质差异
Web 应用的默认假设是「页面短暂、在线常在」:刷新即清空内存,状态可以随时从服务端重建。Electron 桌面应用把这个假设整个翻转过来,主要体现在五个维度:
- 生命周期长。桌面进程常驻数天甚至数周,没有「刷新」这个天然的内存回收点。Web 中习以为常的「内存态缓存 + 短 TTL」策略在桌面端会导致缓存放大、内存持续增长,桌面端更需要主动的缓存失效与版本协调(这是从生命周期差异推导出的架构推论,非单一来源结论)。
- 多进程分片。Electron 中每个
BrowserWindow是独立渲染进程,各自的内存状态天然隔离;而 Web 同源页面可以通过localStorage、BroadcastChannel共享状态。桌面端跨窗口共享服务端状态,必须显式经过 IPC 或共享的存储层(见 §4.3)。 - 有主进程这个特权层。主进程拥有
session、net、safeStorage等模块,能以 OS 级能力持有网络栈与凭据;渲染进程默认被 sandbox 与 contextIsolation 约束(官方安全教程确认:nodeIntegration 默认关闭始于 5.0.0,contextIsolation 默认开启始于 12.0.0,sandbox 默认开启始于 20.0.0)。 - 离线是常态而非异常。笔记本合盖、Wi-Fi 切换、睡眠唤醒,都会造成长连接中断。官方
net文档明确说明:net.isOnline()返回false时「强烈表明无连接」,返回true也不能确定有连接(Rounded to the nearest known state)。 - 版本发布节奏慢。桌面应用经由安装包分发,用户可能停留在旧版本数周,API 与协议兼容窗口必须比 Web 更宽(通用工程共识,无单一权威来源)。
Slack 的桌面重建(2019)是这些差异的经典案例:他们把每个 workspace 建模为一个独立的 Redux store,其中包含「workspace 数据、客户端连接状态、用于实时更新的 WebSocket」;并用 redux-electron 让 store 跨进程同步、用 redux-observable 把 store 变成「跨 Chromium 进程的事件总线」。启动性能上,他们用「CDN 缓存的 HTML + 持久化 Redux store + Service Worker」把启动时间从约 5 秒降到 1 秒以内,同时获得了离线运行能力。
2. 2026 年当前的最佳实践
2.1 服务端状态库(TanStack Query / RTK Query / SWR):仍然只用「查询层」,把网络下沉到 IPC
TanStack Query(v5)、RTK Query(Redux Toolkit 内置)、SWR(Vercel)三者在 Electron 渲染进程内的用法与 Web 几乎一致——它们管理的是「查询缓存 + 重取策略」,本身不关心传输。本文采用的组织方式是:
- 保留这些库的缓存/失效/乐观更新语义,把
queryFn桥接到主进程。这三个库均无官方 Electron 专用文档(截至本文写作,其官方文档无 Electron 章节,此为对官方文档站的检索结论),社区通行做法是通过contextBridge暴露的invoke通道发起请求,即queryFn: ({ queryKey }) => window.api.fetch(...)。 - 渲染进程直接
fetch服务端在技术上是可行的(渲染进程具备 Chromium 网络栈),但会丢失三样东西:session/cookie 的集中控制、系统代理的一致行为、凭据不进渲染进程的安全边界。若坚持直连,token 必须出现在渲染进程内存中,与官方安全教程「渲染进程只加载可信内容、IPC 校验 sender」的模型相抵触。 - 注意多窗口行为:每个窗口一个
QueryClient实例时,refetchOnWindowFocus在窗口聚焦时各自触发,重复请求会翻倍。单实例主进程代理天然去重;或者按官方渲染优化指南(tracked properties、structural sharing、select)控制重渲染成本。
2.2 网络层:渲染进程直连 vs 主进程代理
官方 net 模块文档给出了关键事实:
net模块(含net.fetch)只能在主进程与 Utility Process 使用,且必须在 appready之后;它使用 Chromium 原生网络栈,相比 Node 的http/https自动获得系统代理(含 WPAD/PAC)、HTTPS 隧道、认证代理(basic/digest/NTLM/Kerberos/negotiate)支持。net.fetch请求默认走 default session;要绑定特定 session 需用ses.fetch()(Session 实例上的 fetch)。这决定了「cookie 隔离的多账号请求」应通过session.fromPartition('persist:account-x').fetch()发出。net.WebSocket仅主进程可用;Utility Process 中不支持自定义协议的拦截。- 重计算或长时间网络工作可放进
utilityProcess.fork()(官方文档:等价于 Nodechild_process.fork,但基于 Chromium Services API,支持MessagePortMain通信)——这是 本分册采用的「同步引擎跑在独立进程」载体。
推荐架构(综合官方文档与社区实践):
Renderer(N 个) ──ipcRenderer.invoke──▶ Main(或 UtilityProcess)
├─ session.fromPartition('persist:acct-a').fetch(...)
├─ net.fetch / net.WebSocket(单条长连接)
└─ safeStorage 加解密 token2.3 cookie/session 处理与多账号
session.fromPartition(partition):partition 以persist:开头则为持久 session(磁盘落盘,重启保留),否则为内存 session;同名 partition 复用已有实例,已创建的 Session 无法再改 options(官方文档原文)。较新版本还提供session.fromPath()可显式指定存储路径。- Cookie 通过
session.cookiesAPI 读写;未设置expirationDate的 session cookie 即使在persist:partition 中也不会跨应用重启保留——这是 electron/electron#9995 记录的长期行为,做「记住登录」时不要依赖 session cookie,应持久化 refresh token(见 §2.4)。 - 多账号的标准做法:每个账号一个
persist:<accountId>partition,各自隔离 cookie、缓存、localStorage;账号切换即切换 partition 或新建窗口。Slack 的 workspace 模型(每 workspace 一个 store + 独立连接状态)与这一思路同构。
2.4 OAuth 桌面端落地:授权码 + PKCE + 深链接/loopback
RFC 8252(OAuth 2.0 for Native Apps,BCP 212)是权威依据,要点:
- 必须用外部用户代理(系统浏览器)发起授权,不使用嵌入式 webview(防钓鱼/凭据窃取)。
- 公共客户端必须使用 PKCE(Section 8.1 原文:"Public native app clients MUST use PKCE"),这与 redirect 方式无关。
- 三种回跳方式:①私有 URI scheme(自定义协议,任何应用都可注册同一 scheme,故需 PKCE 兜底);②App-claimed
httpsURI(OS 基于域名所有权保证目标应用身份,"SHOULD use them over the other options where possible",但在桌面端 OS 支持有限,待核实各平台现状);③loopback interface redirect(http://127.0.0.1:<port>,桌面端推荐)——用字面 IP127.0.0.1而非localhost、先绑定监听再开浏览器、收到一次授权响应后立即关闭监听、不要设置SO_REUSEADDR/SO_REUSEPORT(RFC 8252 §7.3 与 §B.5)。
Electron 侧落地:
- 自定义协议注册:
app.setAsDefaultProtocolClient('myapp')(开发态需传process.execPath)。macOS 走open-url事件;Windows/Linux 冷启动时深链接出现在process.argv、热运行时触发second-instance事件——必须配合app.requestSingleInstanceLock()单实例模式,否则点一次链接拉起一个新实例。注意 Mac App Store 版对 single instance lock 有已知崩溃问题(2025 年一线工程博客 bloomca.me 记录,对应 Electron issue)。 - 应用内部协议(非 OS 级):
protocol.registerSchemesAsPrivileged()(必须在 app ready 之前且只能调用一次)+protocol.handle()(返回Response或Promise<Response>)。 - refresh token 存储:存主进程,用
safeStorage加密后落盘。当前官方 API 文档推荐是异步 API(encryptStringAsync/decryptStringAsync)——官方文档明确「推荐 async 优先于 sync,sync 未来可能被弃用」,async 版本支持 key rotation(shouldReEncrypt)与临时不可用处理(isTemporarilyUnavailable)。平台语义必须清楚:macOS 需 Keychain(应用必须稳定签名,否则每次更新 Keychain 都会重新弹授权框);Linux 依赖 kwallet/gnome-libsecret,无 keyring 时回退basic_text后端(硬编码口令加密),形同明文——可用safeStorage.getSelectedStorageBackend()检查;Windows 用 DPAPI。safeStorage的保护边界是「同机其他用户/其他应用」,防不了本用户自己。Clerk 的 Electron SDK 即采用「electron-store + safeStorage」组合,并在持久化不可用时退回进程内存(其官方文档明示该行为),可作为参考实现。 - Access token 只留主进程内存,绝不写盘、不进渲染进程。
2.5 实时同步:单连接、广播、断线重连、乐观更新
- 一条连接原则:N 个窗口各开一条 WebSocket 是桌面端经典反模式(连接数、服务端扇出、状态不一致同时放大)。采用方式与适用条件:长连接放在主进程(
net.WebSocket)或 Utility Process,SSE 亦然;变更事件经webContents.send()广播到各窗口,渲染进程把推送当作 TanStack Query 的setQueryData/invalidateQueries信号。 - 高吞吐直连通道:窗口间高频同步(如编辑器状态)可跳过主进程中转——官方 MessagePorts 教程给出
MessageChannelMain方案:主进程创建 port1/port2 分别postMessage给两个窗口,之后两窗口直接通信、不再经过主进程;注意MessagePort只能通过postMessage转移,send/invoke均不可。Slack 则用 redux action 流本身做跨进程广播(redux-electron),本质相同:事件总线 + 各进程本地 store 归约。 - 断线重连:以请求失败/心跳超时为信号做指数退避重连,不要依赖
navigator.onLine/net.isOnline()作为唯一判据(官方文档明示true不代表真有连接)。睡眠唤醒后应主动探测并重建连接。重连成功后的状态追补(catch-up)用「最后事件游标」增量拉取,避免全量刷新。 - 乐观更新:TanStack Query 的
useMutation+onMutate写缓存、onError回滚的 Web 模式可直接复用;桌面端额外要求是把「待确认的乐观变更」持久化到本地队列,否则进程崩溃/退出会造成静默丢失(见 §3 离线队列)。
3. 离线优先架构:本地队列、同步引擎、冲突解决
3.1 分层判断
PowerSync 官方博客给出了一个务实的判断框架(2025):只同步单用户自己的数据时,通常不需要同步引擎——本地队列(如 SQLite/IndexedDB 里的 outbox 表)+ 幂等重放 + updated_at 乐观锁就够;当涉及多设备/多人并发写同一份数据时,自建冲突解决的复杂度才会失控,此时才值得引入同步引擎。当前三大「可插拔」同步引擎是 ElectricSQL、Zero、PowerSync(PowerSync 博客原文),另外 Replicache 处于维护模式、Automerge/Yjs 等 CRDT 库覆盖协作文档场景。
3.2 2025–2026 关键动态(时间线均有来源)
- Replicache → Zero:Replicache 官网状态声明:五年后进入维护模式,代码已开源、不再收费,官方建议用户迁移到 Zero。Zero 1.0 于 2026 年 6 月发布(InfoQ 报道):架构为
zero-client(应用内客户端库)+zero-cache(Postgres 只读副本服务),查询语言 ZQL 本地命中下一帧返回、权威结果后台同步;定位是「只是一个 fancy cache」。离线支持暂 out of scope(Rocicorp 创始人 Aaron Boodman 在 2025 Local-First Conference 的表态,转引自 PowerSync 博客)。 - ElectricSQL:1.0 GA 于 2025-03-17(localfirst.fm landscape 记录)。定位收窄为 Postgres read-path 同步(partial replication / fan-out / HTTP Shape 交付),不做写路径与客户端持久化——离线写需开发者自建。2026-08-11 Databricks 宣布收购 Electric(The New Stack 报道),收购后其能力(PGlite/同步引擎)转向 AI agent 场景(Lakebase),Electron 应用选型时需评估其通用同步路线的持续性。
- PowerSync:SQLite-everywhere 路线——移动端原生 SQLite 绑定,Web 端 WASM SQLite + OPFS/IndexedDB;三者中唯一一等公民支持离线(含离线写队列),2024 年起大规模生产使用(kanopylabs 2026 对比文)。Electron 中通常以 WASM SQLite 方案接入渲染进程(官方对 Electron 主进程专用 SDK 的支持状态:待核实)。
- Automerge 3.0(2025-08 发布):Rust 核心 + WASM,运行时改用压缩列式表示,内存占用降低 10 倍以上(官方博客:Moby Dick 文档从 700MB 降至 1.3MB),文件格式与 v2 兼容。注意事项:WASM 32 位内存模型带来 4GB 硬上限(Cinapse 迁移复盘)。Yjs 仍是文本协作的常用选择。
3.3 选型对比
| 方案 | 同步方向 | 离线写 | 冲突解决 | 后端要求 | 2026-10 状态 |
|---|---|---|---|---|---|
| 自建队列(outbox + 幂等重放) | 双向(自己写) | ✅ 自己实现 | 自定义(LWW/版本号) | 任意 | 始终可行,单用户数据首选 |
| PowerSync | 双向 | ✅ 一等公民 | 服务端业务规则(custom upload queue) | Postgres/MongoDB/MySQL 等 + sync service | 生产成熟度最高(三方中) |
| ElectricSQL | 只读为主(read-path) | ❌(需自建) | 不涉及(不做写路径) | Postgres | 1.0(2025-03);2026-08 被 Databricks 收购,路线待观察 |
| Zero(Rocicorp) | 读同步 + 经自有写路径 | ❌ 暂 out of scope | 服务端写路径自定义 | Postgres + zero-cache | 1.0(2026-06) |
| Replicache | 双向 | ✅ | server reconciliation | 任意(自实现 push/pull) | 维护模式,建议迁 Zero |
| Automerge 3 / Yjs(CRDT) | 文档级双向 | ✅ | CRDT 自动合并 | 自带 sync 协议,传输自建 | Automerge 3.0(2025-08) |
桌面端视角的建议:Electron 主进程是完整 Node 环境,同步引擎/SQLite 可放主进程或 UtilityProcess,多窗口天然共享同一副本;若选 Web 系方案(Electric/Zero/PowerSync WASM)则跑在渲染进程,需自行解决「多窗口多副本」问题(要么单窗口架构,要么把引擎上移到主进程经 IPC 订阅)——这是 Electron 与纯 Web 最大的集成差异。
3.4 冲突解决思想速览
- LWW(last-write-writer-wins):实现最简单,适合幂等、可覆盖的字段;丢弃并发写。
- 版本向量/updated_at 乐观锁:服务端拒绝过期写,客户端拉取-合并-重试;大多数业务型应用够用。
- server reconciliation(Replicache 模式):客户端推 mutation 序列,服务端按序重放并返回最终态,客户端本地回滚/重放差异(Replicache 官网描述为「来自多人游戏的技术」)。
- CRDT:自动合并、无协调,适合协作文档/白板;代价是数据模型受 CRDT 类型约束、历史元数据体积(Automerge 3 已大幅缓解)。Cinapse 团队的复盘值得参考:他们最终从 CRDT 迁回 PowerSync,理由是业务同步需要可预测的服务端规则而非自动合并。
4. 常见坑与反模式
- token 放渲染进程/localStorage。桌面渲染进程加载的代码一旦被注入(依赖供应链、XSS 于远程内容),凭据即泄露。正确做法:token 只存主进程内存,refresh token 用
safeStorage异步 API 加密落盘。 - 在 embedded
BrowserWindow/webview 里做 OAuth 登录页。违反 RFC 8252(应外部浏览器);且登录页脚本与主 frame 同权限模型,风险大。若必须应用内展示,用独立 partition 的临时窗口并在will-navigate里严格校验 URL(官方安全教程:不要用startsWith判断域名)。 - 深链接只处理了热路径。只监听
second-instance而忘了冷启动时解析process.argv(Windows/Linux),或 macOS 没监听open-url——冷启动深链接会打开默认视图。 registerSchemesAsPrivileged在 ready 之后调用或调用两次。官方文档明确:只能在ready之前、且只能调用一次,否则自定义协议行为异常。- 依赖 session cookie 实现免登录。
persist:partition 不保留无过期时间的 session cookie(electron/electron#9995),应用重启即登出。 - Linux 上假定
safeStorage一定安全。无 keyring 的发行版回退basic_text(硬编码口令),加密形同虚设;应检查getSelectedStorageBackend()并降级策略(不持久化/提示用户)。 - macOS 未签名/签名不一致就发版。Keychain 会把不同签名的构建当作不同应用,每次更新都弹授权框(官方文档明示)。
- 每窗口一条 WebSocket + 每窗口各自轮询。连接数与数据不一致双重放大;收敛到主进程单连接 +
webContents.send广播。 - 用
navigator.onLine驱动重连。官方文档明示其不可靠(true不代表可达);以实际请求失败为准。 - 乐观更新不落盘。桌面应用被强杀/崩溃时未确认的乐观变更静默丢失;outbox 落盘 + 重放幂等键。
- 无界缓存。常驻进程 +
staleTime: 0的后台重取会让缓存只增不减;桌面端应设gcTime上限并对大列表做分页/虚拟化(TanStack Query 通用机制,桌面端尤甚)。 - 把
ipcRenderer.on原样暴露给页面。官方安全教程第 20 条:暴露原始 IPC API 等于泄露整个 IPC 系统,应包装回调只传值,并校验e.senderFrame.origin。
5. 关键代码示例
5.1 主进程:OAuth 深链接 + loopback 回调 + safeStorage 存 refresh token
// main.js —— 节选(示意)
const { app, shell, safeStorage, session, net } = require('electron')
const crypto = require('node:crypto')
const http = require('node:http')
const SCHEME = 'com.example.myapp'
// 1) 单实例锁:深链接在 Win/Linux 走 second-instance
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) app.quit()
app.on('second-instance', (_e, argv) => {
const link = argv.find(a => a.startsWith(`${SCHEME}://`))
if (link) handleAuthRedirect(link)
})
// macOS:冷/热启动都走 open-url
app.on('open-url', (e, url) => { e.preventDefault(); handleAuthRedirect(url) })
async function startOAuth() {
const verifier = crypto.randomBytes(32).toString('base64url')
const challenge = crypto.base64url(
crypto.createHash('sha256').update(verifier).digest()) // S256
const state = crypto.randomBytes(16).toString('hex')
// RFC 8252 §7.3:loopback 优先,用字面量 127.0.0.1,先起监听再开浏览器
const server = http.createServer((req, res) => { /* 校验 state,取 code */ })
await new Promise(r => server.listen(0, '127.0.0.1', r)) // 0 = 随机端口
const port = server.address().port
await shell.openExternal(
`https://auth.example.com/authorize?response_type=code` +
`&client_id=...&redirect_uri=http://127.0.0.1:${port}/cb` +
`&code_challenge=${challenge}&code_challenge_method=S256&state=${state}`)
// 换 token 后:refresh token 加密落盘(异步 API 是官方推荐)
}
async function persistRefreshToken(token) {
if (await safeStorage.isAsyncEncryptionAvailable()) {
const buf = await safeStorage.encryptStringAsync(token)
require('node:fs').writeFileSync(tokenPath, buf)
} else {
// Linux basic_text / Keychain 不可用:选择不持久化,要求重新登录
}
}5.2 多账号 partition + 主进程代理请求(TanStack Query 的 queryFn 桥接)
// main.js
const { session, ipcMain } = require('electron')
ipcMain.handle('api-fetch', async (_e, accountId, path, init) => {
// 校验 sender 来源(官方安全教程第 17 条)
const ses = session.fromPartition(`persist:acct-${accountId}`)
return ses.fetch(`https://api.example.com${path}`, init) // 绑定该账号的 session(cookies 随行)
})
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
fetch: (accountId, path, init) =>
ipcRenderer.invoke('api-fetch', accountId, path, init)
})
// renderer —— TanStack Query 只管缓存语义,传输全部走 IPC
const query = useQuery({
queryKey: ['projects', accountId],
queryFn: () => window.api.fetch(accountId, '/projects').then(r => r.json()),
})5.3 主进程单 WebSocket + 多窗口广播 + 乐观更新信号
// main.js:长连接在主进程(net.WebSocket 仅主进程可用;此处为示意,完整选项见官方 net 文档)
const { BrowserWindow } = require('electron')
function startRealtime(lastVersion) {
const ws = new net.WebSocket('wss://rt.example.com/stream')
ws.addEventListener('message', ({ data }) => {
const event = JSON.parse(data)
// 广播到所有窗口:各窗口 QueryClient 消费同一事件
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send('remote-event', event)
}
})
// 指数退避重连;不要用 net.isOnline() 作为唯一依据
}
// renderer:推送 -> 写缓存;乐观更新在 onMutate 写、onError 回滚
import { QueryClient, useMutation } from '@tanstack/react-query'
window.electron?.onRemoteEvent(evt => {
qc.setQueryData(['projects', evt.projectId], evt.project) // 或 invalidateQueries
})6. 参考来源
- Electron 官方文档 · session — https://electronjs.org/docs/latest/api/session
- Electron 官方文档 · net — https://electronjs.org/docs/latest/api/net
- Electron 官方文档 · protocol — https://electronjs.org/docs/latest/api/protocol
- Electron 官方文档 · safeStorage — https://www.electronjs.org/docs/latest/api/safe-storage
- Electron 官方文档 · utilityProcess — https://electronjs.org/docs/latest/api/utility-process
- Electron 官方教程 · IPC — https://electronjs.org/docs/latest/tutorial/ipc
- Electron 官方教程 · MessagePorts — https://electronjs.org/docs/latest/tutorial/message-ports
- Electron 官方教程 · Security — https://electronjs.org/docs/latest/tutorial/security
- Electron 官方博客 · Electron 37.0.0 — https://electronjs.org/blog/electron-37-0
- Electron 官方文档 · Electron Releases(8 周节奏 / 支持最近 3 个稳定版) — https://electronjs.org/docs/latest/tutorial/electron-timelines
- electron/electron issue #9995 · persist session cookies — https://github.com/electron/electron/issues/9995
- RFC 8252 · OAuth 2.0 for Native Apps (BCP 212) — https://www.rfc-editor.org/info/rfc8252
- bloomca.me · Custom Protocols and Deeplinking in Electron apps (2025-07) — https://blog.bloomca.me/2025/07/20/electron-apps-custom-protocols.html
- BigBinary · Building deep-links in Electron application — https://www.bigbinary.com/blog/deep-link-electron-app
- Slack Engineering · When a rewrite isn't: rebuilding Slack on the desktop (2019) — https://slack.engineering/rebuilding-slack-on-the-desktop
- Slack Engineering · Growing Pains: Migrating Slack's Desktop App to BrowserView — https://slack.engineering/growing-pains-migrating-slacks-desktop-app-to-browserview
- Slack Engineering · Service Workers at Slack — https://slack.engineering/service-workers-at-slack-our-quest-for-faster-boot-times-and-offline-support
- TanStack Query 官方文档 · Render Optimizations — https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations
- Replicache 官网 · Status(维护模式声明) — https://replicache.dev
- InfoQ · Zero Reaches 1.0 (2026-06) — https://www.infoq.com/news/2026/06/zero-version-1
- PowerSync Blog · Offline-First Apps Made Simple: Supabase + PowerSync — https://powersync.com/blog/offline-first-apps-made-simple-supabase-powersync
- localfirst.fm Landscape · ElectricSQL — https://www.localfirst.fm/landscape/electricsql
- The New Stack · Databricks acquires Electric (2026-08-11) — https://thenewstack.io/databricks-electric-wasm-agentic-postgres
- Kanopy Labs · Electric SQL vs PowerSync vs LiveStore: Local-First Sync 2026 — https://kanopylabs.com/blog/electric-sql-vs-powersync-vs-livestore-local-first
- Automerge 官方博客 · Automerge 3.0 (2025-08) — https://automerge.org/blog/automerge-3
- PowerSync Blog · Why Cinapse Moved Away From CRDTs For Sync — https://powersync.com/blog/why-cinapse-moved-away-from-crdts-for-sync
- Clerk Docs · storage() (Electron) — https://clerk.com/docs/reference/electron/storage
- ROCICORP · Retiring Reflect(Replicache/Zero 路线背景) — https://rocicorp.dev/blog/retiring-reflect
最后更新于