@baukit/sync-client

中文文档
0.18.3发布于 2026/11/07MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 8082

客户端列表,传输,状态报告,并订购离线能力同步回路.

中文使用文档
文档目录14 节
  1. 混合逻辑时钟
  2. 时钟迁移
  3. 调度器
  4. 重试和后退
  5. 浏览器环境
  6. 展览环境
  7. 运输
  8. 状态存储
  9. 冷的开始
  10. 推出批量排行榜
  11. 有效载荷验证器
  12. 遵守规则
  13. 墓碑平面
  14. 通石-平面迁移

@baukit/sync-client

@baukit/sync-client 持有客户端的机械, 线下能力产品重建 同步回路:调度,运输,状态,按压顺序,以及混合逻辑时间戳. 没错 不定义同步协议 。 终点路径、有效载荷、冲突规则和同步引擎本身 产品自有,作为 离线准备合同 他们说 必须.

根项没有运行时间依赖性, 也没有反应 。 计时器 前景状态 连接, 财务报告和审定财务报表 fetch 全部通过注射到达,所以同一个代码运行在节点下,一个浏览器,以及React Introduct. 可选 @baukit/sync-client/expo 进口 react-native 财务报告和审定财务报表 expo-network; 也没有 由根条目加载。 这个 @baukit/sync-client/browser 条目仅在 createBrowserSyncEnvironment 所以在节点下导入是安全的.

混合逻辑时钟

从根包导入时钟或 @baukit/sync-client/hlc。它使用相同的整数 编码为 baukit_sync::hlc,因此Rust和TypeScript可以直接比较存储的时间戳.

import { HybridLogicalClock } from '@baukit/sync-client/hlc';

const clock = await HybridLogicalClock.open('device-a', metadataStore, Date.now);
const localTimestamp = await clock.now();
const afterRemote = await clock.observe(remoteTimestamp);
const state = clock.snapshot();

编码是 wallTimeMs * 1000 + counter + 1。增加的1个保留为无效。 使用 encodeHybridLogicalTimestamp, (中文(简体) ), decodeHybridLogicalTimestamp,以及 compareHybridLogicalTimestamps 当不需要时钟。 比较返回 null 若为 输入无效.

JavaScript 只能通过 Number.MAX_SAFE_INTEGER, (中文(简体) ), 9_007_199_254_740_991。最后一个有效的 HLC 值解码为墙壁时间 9_007_199_254_740 财务报告和审定财务报表 计数器 990超过此点的呼吁 HybridLogicalClockError 代码 exceeds_safe_integer 并保持原有状态。 在正常范围内,1 000台 将壁构件向前移动一毫秒,并将计数器重置为零.

设备ID和物理时钟是呼叫器输入. 该模块不生成设备ID或 选择存储系统。 无法通过存储时钟或执行 HlcStorage 产品 持续。 存储状态只有在设备ID匹配且组件构成一个时才会恢复 有效的编码时间戳。 失踪 腐败 外国重置为零 存储读写 失败拒绝不变.

当存储存在时, now 财务报告和审定财务报表 observe 以呼叫顺序运行。 稍后的呼叫等待前一个 写完 写入失败不更新快照,也不停止队列 . snapshot 因此,报告最后一个承诺储存的国家.

时钟迁移

Redemut 可以将其本地 HLC 导入替换为 @baukit/sync-client/hlc。现有的 open(deviceId, storage, physicalClock) 电话和 hlc-state 记录保持兼容。 没有存储 需要时间戳或状态重写。 Redemut 保留其索引DB和 SQLite 适配器,设备ID 创建、合并规则、服务器比较和扫描以及光标协议.

调度器

SyncScheduler 一个不透明 run() 回调和 a SyncSchedulerEnvironment它保证 单个活动跑道:一个到达中跑道的触发器会加入运行而非启动第二个运行道.

const scheduler = new SyncScheduler(() => engine.run(), environment, { intervalMs: 60_000 });
scheduler.start();

trigger() 启动运行或加入活动运行. requestFollowUp() 当地写作时 如果运行已激活, 它会排队在运行完成后重播一次, 所以会写 引擎是中推的, 从来没有被忽略。 重复的跟进请求仍然失败 进入一次重播. stop() 将已排队的重放与间隔和订阅一起放下.

环境报告前景状况和连通性。 排程器启动时 应用程序返回到前景,当连接返回时,并在间隔时返回。 它安装不 间距,同时背景化并忽略那里的连接事件.

失败运行到达 onError 并且不停止计划。 无 retry,下一次尝试 等待下一个触发器.

重试和后退

过 retry 在延迟后重试失败的运行。 产品决定哪些失败值得 重试和等待多久, 因为只有它知道它的错误类型; 调度器拥有尝试 数起,等待, 并提前唤醒它. fullJitterBackoffMs 从 @baukit/api-runtime/backoff 实值 baukit的延迟政策。 这个软件包的根入口 仍然没有进口到任何东西 所以产品 政策通过如下:

import { fullJitterBackoffMs } from '@baukit/api-runtime/backoff';
import { SyncRateLimitError, SyncScheduler, SyncTransportError } from '@baukit/sync-client';

const scheduler = new SyncScheduler(() => engine.syncOnce(), environment, {
  retry: {
    maxRetries: 4,
    delayMs: (error, retryIndex) => {
      if (error instanceof SyncRateLimitError) return Math.max(0, Date.parse(error.retryAt) - Date.now());
      if (error instanceof SyncTransportError && !error.retryable) return null;
      return fullJitterBackoffMs(retryIndex, { baseDelayMs: 500, maxDelayMs: 30_000 });
    },
    onRetryScheduled: ({ delayMs }) => status.showRetryAt(Date.now() + delayMs),
  },
});

运行失败后, 调度员询问 delayMs(error, retryIndex)。 。 。 . null 意思是不要重试 数字 是数以毫秒计的等待 onRetryScheduled 在等待开始前听说了 这个 重试索引从 0 计数,成功后重新启动 。 之后 maxRetries 重试调度器 等待下一个触发器。 消极或不有限的延迟,或 delayMs 抛出, 达到 onError 并结束复试。 A 类 maxRetries 这不是一个非负整数抛出一个 RangeError 从建筑师.

等待是一个一发计时器 从环境的 setInterval,所以没有环境 变化。 等待中的任何触发器一次结束它, 并在 0 重新开始重试索引: 手动 trigger()中,间隔,返回前景,或连接返回 前观相. 等待中的后续请求不会结束,因为重试会发送 同样的写作. stop() 结束等待而不运行, 和等待结束 当应用程序是 在应用程序返回前, 背景未运行 .

如果引擎在自己内部等待重试延迟,则通过 onRecoverySignal 到调度器 在那里唤醒延迟。 调度员打电话来 active 在前方触发运行之前 online 在网络触发运行之前。 在线恢复报告,即使调度器是 的背景, 尽管排程器仍然在等待前景, 然后再开始另一次运行 . 重新尝试唤醒属于此调度器选项, 而不是在装饰环境中, 因为 环境不知道引擎是否有重试延迟.

const scheduler = new SyncScheduler(() => engine.retryNow(), environment, {
  onRecoverySignal: () => engine.wakeRetryDelay(),
});

浏览器环境

浏览器产品可以导入一个绘制文件可见度、在线事件和 排程合同的间隔 :

import { SyncScheduler } from '@baukit/sync-client';
import { createBrowserSyncEnvironment } from '@baukit/sync-client/browser';

const scheduler = new SyncScheduler(() => engine.retryNow(), createBrowserSyncEnvironment(), {
  onRecoverySignal: () => engine.wakeRetryDelay(),
});

isActive 只在 document.visibilityState 实值 visible. 知名度变化报告 新的活动状态,以及 online 事件报告连通性警示信号。 每个人返回 清理功能可能不止一次被调用,并移除其收听器一次.

测试和非浏览器主机可以注入 { document, window, timers }呼唤工厂没有 浏览器外的注入会丢出一个特定的缺失文档或缺失窗口出错. 导入 条目从不读取任何全局。 浏览器条目没有 React, Dexie, 或同步引擎导入 .

展览环境

博览会产品可以使用所提供的环境,而不是重复AppState和网络接线:

import { SyncScheduler } from '@baukit/sync-client';
import { createExpoSyncEnvironment } from '@baukit/sync-client/expo';

const scheduler = new SyncScheduler(() => engine.run(), createExpoSyncEnvironment());

环境只处理 App State active 作为前景。 在本地平台上读取 初始状态从 expo-network ,只有在连接从无法使用到 可用 。 Expo Web 使用浏览器 online 活动。 测试可以通过 { timers } 替换 setInterval 财务报告和审定财务报表 clearInterval。 。 。 . react-native 财务报告和审定财务报表 expo-network 是可选的同位素,因为 非Expo 消费者不会导入此子路径 .

运输

获取备份 SyncTransport 构造器将所插入的 fetch 和认证信头提供者. 它将故障分类如下:

  • SyncAuthError 401, 永远无法重试
  • SyncPartitionMismatchError 当主体携带配置的分区匹配代码时
  • SyncRateLimitError 429, 总是可以重试和携带的 retryAt 加上生的 retryAfter 标题
  • SyncNetworkError 何时 fetch 或响应正文读取失败, 总是可以重试
  • SyncServerError 用于其他 HTTP 失败, 408 和 5xx 可重试
  • SyncPayloadCompatibilityError 当成功响应无效时, JSON 将永远无法重试

Retry-After 接受整数三角洲秒和 HTTP 日期。 失踪、无效、负和过去 使用值 now + retryAfterFallbackMs。默认倒计时为60秒。 测试和产品 使用其它策略可以注入 now 财务报告和审定财务报表 retryAfterFallbackMs 在后置构造器中.

每个请求都有期限, requestTimeoutMs,默认为30秒(DEFAULT_REQUEST_TIMEOUT_MS) (中文(简体) ). 它覆盖取来和身体读取。 当它经过时, 传输中断请求的信号 且以能试验的、 SyncNetworkError因此,调度器的下一次运行打开一个新的 而不是等待一个集合 的包被丢弃。 两个建筑师都接受 环境。 预建的请求函数收到截止日期信号,应传递给 联合国 fetch来电者自己 signal 仍然中止请求.

Auth 信头每次尝试都会解析, 所以在重试之间刷新一个符号会被取出 。 这个 分区头只在呼叫者通过时发送 partitionId,以及它的名称和 不匹配代码可以配置 。 运输从不命名终点:呼叫者通过路径 反应类型.

一个产品API客户端经常已经拥有了基础URL,符号刷新,认证恢复,, 响应解码及其出错政策。 通过它的解码请求函数而不是复制 在运输中工作的:

const transport = new SyncTransport({
  request: apiClient.request.bind(apiClient),
  partitionHeader: 'X-Product-Profile-Id',
});

await transport.request('/api/v1/sync/push', {
  method: 'POST',
  body: { changes },
  partitionId: profileId,
});

这个模式构建了相对路径,查询字符串,JSON正体,以及分区标题,然后是代表. 请求功能拥有解码和出错, 它保留了产品 API 客户端现有的认证 恢复完好无损 产品需要软件包自主响应时使用后置构造器 分类.

状态存储

SyncStatusStore 是可观测同步状态,与 subscribe编号 :getSnapshot 没有状态 图书馆。 它编码了离线准备合同的要求 产品不断得到 错误:

  • 可诉拒绝排在运输失败之后,所以对现有冲突失败 仍读 attention· ;
  • 未完成的工作比纯错误高, 所以未完成的运行还有待决的修改 pending, (中文(简体) ), 没有 error· ;
  • 第一次运行是初始拉出;后来运行是固定的活动;
  • 失败更新 lastAttemptAt 不更改 lastSuccessAt 或清除 pendingCount。 。 。 .

打电话 setSyncing(attemptAt) 当一个尝试开始. setIdle(successAt) 记录成功 尝试和清除 只有在产品的成功 上游通过. setFailure 接受 具有其中一种的机器可读元数据: auth, (中文(简体) ), partition_mismatch, (中文(简体) ), rate_limited, (中文(简体) ), network, (中文(简体) ), server, (中文(简体) ), payload_compatibility,或 local_apply。产品副本留在 单独 error 字段.

速率限制元数据包含 retryAt。其他可重试的失败可以通过 retryAt 输入 SyncFailureUpdate 参数或调用 setRetrying(retryAt)。快照暴露了两者 retrying 财务报告和审定财务报表 retryAt,因此屏幕可以区分计划重试和速率限制与离线状态.

refreshRevision 每一次运行都会发出新的权威快照. deriveLocalStoreReadiness 把它和消费者实际制造的东西比起来 寒冷的起步如何避免在仍然到达的数据上闪出一空状态. deriveInitialSyncState 将快照映射到 unknown, (中文(简体) ), syncing, (中文(简体) ), offline-cached, (中文(简体) ), sync-error-cached,或 settled。仅进行首跑 network 失败地图到 offline-cached。 。 。 .

该商店是通用的,高于它的注意力项,默认为现有的 { entityType, entityId } 形状。 这样可以保留一个商店 API,同时允许产品保留 拒绝详细列出其用户界面需求 :

interface ProductAttention {
  objectEntityType: string;
  objectEntityId: string;
  rejections: readonly Rejection[];
}

const status = new SyncStatusStore<ProductAttention>();
status.setAttention(items, pendingCount);

冷的开始

坚持尝试和成功时间戳,然后用物体进行水合:

status.hydrate({
  lastAttemptAt: persisted.lastAttemptAt,
  lastSuccessAt: persisted.lastSuccessAt,
  attention: persisted.attention,
  pendingCount: persisted.pendingCount,
});

推出批量排行榜

rankPushBatch 命令一个推出批次 这样父母就永远不能送孩子了.

const rank = dependencyRankByOrder(['container', 'group', 'leaf']);
const batch = rankPushBatch(pending, {
  rank,
  batchSize: 200,
  isHeldBack: (change) => hasUnsentChildren(change),
});

变动按 entityType:entityId。每个组保持其第一组的位置 因此,外国键令 存活在一起, 并带来最新的变化 所以 请求发送当前数据. coveredChangeIds 列出每个队列更改组的结算, 因为服务器结果是由实体而不是排行键入:一个结果清除了全部 他们.

isHeldBack 是规则的另一半。 子女仍未获准的父母 由批次投放,而不是重新订购,一旦这些孩子有再试 已被接受。 现在发送它会显示服务器是完整的父方,而需要的孩子是 失踪。 后置实体不使用批量大小的槽.

实体名称,依附顺序,和被扣回的上游都是产品输入. 宝琪 只知道有军衔.

有效载荷验证器

打电话 validatePushOutcomeCoverage 在确认任何提交的更改之前。 通过一个关键阅读器 然后将所有被接受和拒绝的结果合并在一起 第二个论点。 函数丢出 SyncPayloadCompatibilityError 提交实体 没有结果.

打电话 validatePullPage(currentCursor, page, compare) 之后 hasMore。光标是 不透明。 被注入的参照物定义了它们的顺序. 一个倒退的光标, 一个非无限的比较, 或者说 hasMore 没有光标进度抛出 SyncPayloadCompatibilityError。 。 。 .

commitCursorAfterLocalTransaction 运行提供本地交易。 它引用 commitCursor 只有在承诺解决之后 才能把当地失败包起来 SyncLocalApplyError。 。 。 . 此命令可防止本地应用程序在下拉时跳过远程更改 .

遵守规则

@baukit/sync-client/conformance 出口 createSyncConformanceTests返回纯音效测试 并且没有维特或杰斯特的依赖。 将返回的案件与已经进入的跑者登记在一起 产品:

import { createSyncConformanceTests } from '@baukit/sync-client/conformance';

describe('product sync contract', () => {
  for (const testCase of createSyncConformanceTests(productAdapter)) {
    it(testCase.name, testCase.run);
  }
});

适配器每箱供应两个被隔离的客户端和一个假服务器. 还提供回电 用于发件箱的排列、列出和稳定的待处理ID; 原子提交批次结算; 原子拉动 应用程序和光标存储;待报状态;线条编码和解码;以及 假服务器推出,拉出,播种,快照,和断层注射. 冠状固定变化包含 模糊的实体名称,值,逻辑时间,依赖,和墓碑. 适配器将它们映射到 产品的计划和有效载荷.

案例包括重放、交易回滚、页光标进度、停滞和后退 光标, 完整推后结果, 延迟接受和拒绝答复, 暂停等待 写入、失败后待处理状态、 依赖性安全驱动命令以及两个客户的趋同 一个墓碑。 何时 fullResync 还有5个病例 覆盖了单声道的平地平线 光标 零,父子清理,只取一行,待编辑,拒绝记录,重置回滚, 重置延后, 和重复的停止状态 . wire.decodePush 必须呼叫 validatePushOutcomeCoverage 在返回确认行前。 它返回解码器 rejectedRows 当拒绝包含权威服务器行时.

local.applySubmittedBatchOutcome 接收精确 submitted 排行,原始的推力 response, (中文(简体) ), 已解码的已确认行和拒绝行,以及已解码的已拒绝服务器行。 在一个仓库里 交易必须只结清该呈件所涵盖的待处理身份证,记录可提起诉讼 拒绝,应用安全拒绝行,并进行任何被接受的校正盖章。 一个新的待决 同一实体的行阻断了更换其当地有效载荷的旧反应。 订正 印花是产品政策。 套接字不需要最后的写作者 或特定的修改 类型,但来自更古老的提交文件的印章不得取代较新的本地书写的有效载荷.

当旧的或等同的远程修改到达时,拉回调必须保留一个待处理的本地行, 在同一个交易中执行页光标。 它的注射失败必须退后 显示已显示的行和光标.

墓碑平面

保留有限度墓碑的产品适配器 fullResync 字段。 产品 没有它接收原始的大小写集,不需要修改适配器.

zeroCursor 是产品清晰的全重建光标. decodeStaleCursor 绘制产品 窃听错误 { code: 'resync_required', horizon }。产品反应必须保持代码和 地平线稳定 对于HTTP 409信封,来源值是: error.details.horizonRevision。 。 。 .

isResetSafe 是生产政策的钩. setResetSafe 让孤立的测试客户端输入 安全或不安全的状态. reset 清除服务器支持的行并写入 zeroCursor 在一次交易中, 并保留了有待修复的突变 拒绝记录和可修复的行 它必须尊重 failAfterRows 在交易内部,这样回滚大小写就可以检查没有改变的数据库. readResetCount 让套件组证明, 立即的第二个 stale 反应不会启动 循环.

假服务员的召回只停留测试. purgeTombstones 删除请求的固定行并 返回所有者当前视野. rejectNextZeroCursor 为您输入协议违约 中止条件。 实体登记、删除SQL、保留期和外国钥匙订单 留在产品中.

重置后, 套房会提交保留待处理的工作和从零拉出 。 整个快照必须 不覆盖等待编辑的更新。 它必须删除服务器支持的父、子和只拉行 他们的墓碑已经没有了.

通石-平面迁移

现有的适配器可以省略 fullResync;其类型和案例不变. 一个产品 允许保留有限度的墓碑时,应添加回调作为单位。 保留当前光标 类型和 HTTP 信封,将视野解码为 SyncConformanceStaleCursor,替换任何拆分 “ 清除行, 然后用本地交易设置光标” 路径, 然后再启用这些情况 .

版本与发布记录

@baukit/sync-client 共发布 67 个版本,首次发布于 2026/08/26;过去 12 个月发布了 67 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.18.3latest2026/11/07今天
0.18.22026/11/061 天前
0.18.12026/11/062 天前
0.18.02026/11/052 天前
0.17.12026/11/034 天前
0.17.02026/11/025 天前

关于 @baukit/sync-client 的常见问题

@baukit/sync-client 是什么?

客户端列表,传输,状态报告,并订购离线能力同步回路.

如何安装 @baukit/sync-client?

在项目目录运行 npm install @baukit/sync-client,或使用 pnpm add / yarn add / bun add。

@baukit/sync-client 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@baukit/sync-client 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@baukit/sync-client 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@baukit/sync-client 可以商用吗?

@baukit/sync-client 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@baukit/sync-client 的下载量有多少?

根据 npm 官方下载统计,@baukit/sync-client 在 2026/10/08 – 2026/11/06 这 30 天内被下载 8,082 次,日均约 269 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@baukit/sync-client 还在维护吗?

最新版本 0.18.3 发布于 2026/11/07(今天);至今共发布 67 个版本,其中过去 12 个月发布了 67 个;首次发布于 2026/08/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。