@ai-matrx/realtime
Matrx客户端的 ONE Supabase 实时层. 手写 .channel( 在一个
消费应用是一个代码审查缺陷.
pnpm add @ai-matrx/realtime
此软件包为何存在
"我们整个系统都为此而奋斗" 我们一直挣扎着想建立它 给留言,尝试设置笔记。 直到今天,经过数百小时 ——亚鲁曼,2026-08-29
实时不难,因为签名难。 下结为四行. 很难 因为有8个东西是无形的,直到它们受伤为止 每一个东西都是在这里学的 昂贵的方式——一个被冻结的浏览器,一个失去编辑的注释,一个显示"Ada"的聊天 打了一个小时 这个软件包写下OnCE的八件事,作为代码 让每个消费者都使用.
下面列举的是理论。 它取代了以前生活于
垫口前端 supabase-realtime 技能;技能现在指向这里.
学说
1. 压制你自己的回声——修正-发音
Supabase给你发了自己的信. Postgres 更改没有“ 无法自我” 选项 。 这个
你的回声 UPDATE 来了 50至500毫秒后 您的 REST 响应已经返回了新的
排队 降落时,任何"在飞行中拯救"的旗帜已经清除. 仅使用国旗的压制
总是想念回声.
未压制回放的回放能做什么: 保存 N 的回放在用户输入 N+1 时到达 ~ 脏-局部服务器 - 假冲突 - 冲突 UI 做 O( n2) diff work each compare- 下一个拯救它 —— 它的回声重新武装它。 自我维持主行道饱和.
Over-suppression所做的(幻相冲突,2026-09-13):每个平台表的
version 在 Every 更新上移动——桌面同步印章 file_path,一个食用的工作写作
元数据。 同样的演员,相同的内容, 我们的写待: 旧分类账称之为回声,
客户端保留了旧的号码, 而它的下个比较和扫描丢失了
用于文本等于客户端自身基础的行. 客户已经压制了一个事件
上面有它需要的号码.
包的书写分类账 分类每个有效载荷, 第一修订版 (这行的)
version,从 revisionField默认值 version:
- R1. 比我们持有的旧版本 暂停。 。 。 .
- R2. 修订我们未解决的书写将产生匹配的内容 拥有- echo (比REST反应的回声,以数字识别).
- R3. 与匹配内容相匹配的已经确定的修订 拥有- echo; 差异 ; 偏僻.
- R4型导弹. 任何更新的 总是遥远的 ——同一演员,内容相同,待写与否. 交付是无害的;吞食是下一场幽灵冲突.
只有当没有修订时,时间戳原则才适用:
- 严格年龄
updated_at而不是我们所持有的 暂停,丢入(合并) 倒退updated_at,它失败了下一个保存的乐观锁入了粘滞的虚假冲突. - 内容我们已知已经写出来但尚未解决 拥有- echo。 。 。 .
- 平等
updated_at- 只有内容指纹也匹配时,才能自选;a 同一兆位合作者 写着仍然土地. - 无法解开的时间戳 – 交付. 屈从于送货,从不默不作声地下降.
使用自己的写作号码注册: ledger.begin({ …, revision: expectedVersion + 1 }), (中文(简体) ),
ledger.settle(ticket, { revision: response.version }), (中文(简体) ), ledger.observe({ …, revision })。 。 。 .
和它配对 @ai-matrx/data是因为 guardedUpdate({ rebase }){\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}所以客户还是有点颠簸
已丢失( 已睡标签) 将按当前数字重试, 而不是显示为冲突 .
广播回声由信封处理: 每封信封都带有客户端的会话 ID 和 接收器会自己掉下来.
2. 默认执行回声抑制;选择退出是抑制路径
echoSuppression: false 对于真正需要回溯自身事件的稀有消费者来说
在电线上。 这绝不是默认的,也绝不是其他事情的补救.
3. 至少一进一出
重新订阅、重新加入 以及一个播放一行节目的消费者 同时INSERT也提供
同一逻辑事件两次。 供应a eventKey (或接受默认)和包拆分.
4. 手提箱从不运行在套接字路径上
一个Supabase频道在套接字的回调路径上呼叫您的处理器. 发送的处理器 至 Redux , diffs 内容, 或使 RPC 背向执行, 当一个 暴风雨来了 标签也冻结了 每个历史 Matrx 浏览器锁定是一个变体 此; 以500个已排队事件唤醒的背景标签是经典触发 .
软件包在套接字和您的处理器之间设置了有边框的、命令的和出让的队列 . 每个频道都保留顺序(在下次事件开始前等待一个自动处理器) 每几毫秒向事件循环排出量,深度被封顶——溢出 流出最古老的活儿,说得没错,并引发和解,这是正确的恢复 因为"我们跟不上".
5. 重联是指READ. 实时没有重播 .
插座倒下时发生的每起事件 都会永远消失 没有连接
重新读取会留下一个永久错误的屏幕,看起来完全健康. 所以每个频道
声明 onBackfill,而包 调用它 每个 恢复路径: 重新连接, 标签
醒来,网络恢复,队列排出。 无频道 onBackfill 得到一个大声的警告.
后退是焦躁不安的,有一个 稳定重设: 仅在
频道有 举行 30多岁,永远不要 SUBSCRIBED。在订阅器上重置会扇动
1s层的频道循环将永远进行,每个周期都会发射一个完整的接取.
6. Tab睡眠和app背景是正常的失败,而不是异域的失败
一个背景标签被拖住, 直到套接字闲置; 一个被悬停的电话又回来了
还有一个WebSocket 看着 并且永远不会再出现。 两张地图都进入
同样的醒悟之路: 和解,并重新加入任何不明显健康的东西。 网络使用
visibilitychange; 反应原生用途 AppState (单位:千美元)@ai-matrx/realtime/native) (中文(简体) ).
7. 一个地名渠道
苏帕克的 client.channel(topic) 按专题分列的去向: 两个都选择的特性
"messages" 默默地分享一道并失去对方的束缚. 宣布一个命名空间
相反的,对同名的另一种不同的声明会抛出.
另外,反应19双进取效应和 removeChannel 是aync,所以可以重载
交出了上一个挂载的仍然连接的通道 .on(...) 掷出 "不能添加
posgres changes 订阅后的回调( )。 。 。 .
——TOPIC-SEMANTICS SPLIT——两种运输方式的"主题"并非同一种意思,而且 软件包对他们不同.
- Postgres Changes:该话题只是客户端的连接密钥. 服务器推着
已声明过滤器, 无论频道名称如何 。 所以,每一个连接尝试得到一个 独一无二
实例主题 (单位:千美元)
mx:notes:u1#7)),这使得重载碰撞无法免费上方. - 广播和现场:主题是会议室. 是服务器粉丝的路由键
发送消息。 一个独特的实例主题并不花费什么——它花费 所有的东西数字 :
两个同时声明的浏览器
mx:room:1土地在#1财务报告和审定财务报表#2而永不相见. (以0.1.x为单位发运,由@ai-matrx/messaging; 固定在0.2.0. (中文(简体) )
因此,一个频道 携带广播或存在,它的 在线上逐字宣布主题,以及
管理者保留 每个已宣布的题目,一个基本通道,重新计算: 秒数 open() 联 合 国
主题将其绑定在同一个频道上,频道只有在
最后一个持有人关闭。 重载碰撞是在那里通过推迟真正的拆卸来处理的
转身,所以React的清理 时再运行 重接 而不是赛跑一个Aync removeChannel。 。 。 .
在你打开两个持有者关于一个话题之前,值得知道的后果:
- 他们共享一个频道的配置。 第一个
echoSuppression和存在键胜出,和 a 分歧很大channel.room.config-mismatch诊断——并不是一个默默的惊喜. - 他们在当地互相听到. 同为 Tab 的持有者共享一个客户端 ID 并打开频道
broadcast.self:false所以电线从不回响一个持枪者的相框到它的兄弟姐妹身上 经理 手数send()直接发送给该主题的其他开放持有者(相同的信封,相同) dadup; 永不返回发送方; 不包括生电路通道). 跨塔布交付不变. - 其中一个就是幕后主使.
handle.isLocalLeader()对 仅开放时间最长的持有人,以及onLeaderChange(isLeader)领导力移动时点火,因为 头儿关门了 在领导人身上进行一次性工作(自选、CRDT东道主选举)——两次 持有者每个结论 "我是孤独的,我是东道主" 是两个自动的,最后的snapshot-wins bug. - 他们共享一个存在条目(每个频道一个密钥),所以一个话题需要完全一个存在
出版社。 只读取名册的表面——打字员标签、在线点名单——通过
presence: { publish: false }(或publish: false改为useTyping它看到每个人,停下来 争取进入。 两个出版商很吵channel.room.presence.multiple-publishers警告,不是一个谜. - 每个客户端仍然使用回声抑制,所以一个持有者使用 没有 看它的兄弟们自己的
send()——这是回声压制的要点,不是房间故障. 两个持有者所看到的 每个 远程 同行信息 . - 将 Postgres 带入已合并房间的持单人更改绑定符迫使重建(那些
装订在JOIN有效载荷上,以后无法添加). 现通知如下:
channel.room.rebuild,每个持有者通过回填门调节. 打开混合 {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}频道的持有者一起在山上 {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}绝对不会发生.
8. 选择运输
- 邮资变化 ——RLS必须出闸送出(注,文件,任务)的可持久行. 这个 默认实体同步 .
- 广播 - 无授权微分的电平或高频数据(CRDT更新, 打字、信号. 永远不要手动播放一行您同时INSERT ——每个订阅者 得到它两次.
- 出席情况 - 谁来了 唯以法相为相.
- 每个功能一个频道. Postgres 更改、广播和存在可共享一个;不 运行3.
- 如果桌子上有的话
updated_by(平台)_stamp_actor触发器),每个更新已 识别它的编辑器——你可能根本不需要在场.
9. 外国同行拥有其电线——用 wire: {mode:"raw"}
每一次播放这个软件包都用 Matrx 信封包
(单位:千美元){v, cid, eid, ts, data}),而这个信封使得回声抑制和去除成为可能
一点也不 有些频道无法选择: 另一端是 a 单独发布的程序
它读取了它定义的有效载荷形状, 并重塑它下面的电线 只会阻止对等
在两端都听到我们的声音 产生这种模式的案例是:
Chrome 扩展名 matrx-extension-bridge:<userId> 频道 - 已部署的扩展解析 a
赤线 BridgeEnvelope (单位:千美元){direction, action, requestId, payload, timestamp})有"佐德计划".
const bridge = defineChannelNamespace({
namespace: "extension-bridge",
parts: ["userId"],
description: "matrx-extend ↔ matrx-frontend RPC bridge (foreign wire).",
foreignTopic: "matrx-extension-bridge", // the peer's topic root, used verbatim
});
manager.open({
topic: bridge.topic({ userId }),
wire: { mode: "raw", acceptEchoFromSelf: true },
broadcast: [{ event: "FRONTEND_RPC", onMessage: ({ data }) => route(data) }],
eventKey: (_source, payload) => `${payload.direction}:${payload.requestId}`,
onBackfill: () => {},
});
原始模式在两个方向上都意味着两件事: 把你的有效载荷放在电线上
逐字记录 (与手卷相同的字节) channel.send),以及 从不读取
信封 甚至连装满外壳的载荷 都不行. BroadcastMessage.enveloped
总是 false 在这里.
这是一个命名的降级,它从来不沉默. 信封上有两个保护装置:
- 回声抑制 使用过的
cid生线没有 供应wire.isOwnMessage当 等同位素形状识别它的发送者; 否则设置wire.acceptEchoFromSelf: true状态 服务器的代码broadcast.self:false这是唯一的保护。 供应既不合法 并产生channel.raw.echo-unguarded(警告);明确选择退出者仍宣布自己为channel.raw.echo-accepted(info),因为诊断流必须显示运行的频道 无人守护. - 编剧 键入已关闭
eid没有它,每一次到达都是个明显的关键 供应eventKey- 这个 接受原始有效载荷——或channel.raw.dedup-unguarded(标.
foreignTopic 是同一个想法的主题的一半. 名称空间仍然被宣布
已检查相撞;只有 mx: 根被对等者所取代。 将一个外国主题重命名为
mx:… 不迁移任何东西——它把客户端放在一个房间里,而对等者则保持
总是在听.
原始模式为 自愿过渡它的存在是为了让外国铁丝网的两端可以骑着它
今天的软件包。 当同龄人发回信封时,更改将删除 wire 从
spec ——协调两版,并记录同行频道合同中的顺序.
静态零事件核对表
编译但一无所获的订阅有:
- 这表不在
supabase_realtime出版物。 。 。 . - RLS 过滤事件( 实时是 RLS 授权的 ) , 这是一种功能, 但授予门 (b) 交付).
REPLICA IDENTITY FULL缺少, 您需要旧行数据 或 RLS- on- DELETE .- 表格尚未在 PostgREST/实时缓存中(
notify pgrst, 'reload schema') (中文(简体) ).
用这个
反应
import { RealtimeProvider, useChannel } from "@ai-matrx/realtime/react";
import { defineChannelNamespace } from "@ai-matrx/realtime";
// Once, beside the feature. Declaring twice with a different shape throws.
export const notesChannel = defineChannelNamespace({
namespace: "notes",
parts: ["userId"],
description: "Note rows for one user",
});
function App({ supabase, userId }) {
return (
<RealtimeProvider client={supabase} actorId={userId}>
<Notes userId={userId} />
</RealtimeProvider>
);
}
function Notes({ userId }) {
const { status } = useChannel({
topic: notesChannel.topic({ userId }),
postgresChanges: [
{
event: "*",
schema: "workbench",
table: "notes",
rowId: (row) => String(row.id),
fingerprint: (row) => String(row.content),
onChange: ({ row }) => applyNote(row),
},
],
// Realtime has no replay. This is how the gap gets closed.
onBackfill: () => refetchNotes(userId),
});
return <StatusDot status={status} />;
}
掌上电脑可能是新关闭的每一个渲染器——订阅在频道的键上 形状,而不是处理器的身份, 最新的处理器总是运行的.
每个应用程序一个供应商,一个兄弟包如何搭载它
<RealtimeProvider> 准确的构建一个经理, 经理拥有写分类账
让乐观写作的回声可以识别 树上的第二个供货商是
第二分类账和第二套频道, 所以一个经理自己写的字看起来REMOTE到
其他费用。 Apps挂载一个.
自己做实时工作的包(PACAGE)@ai-matrx/messaging, (中文(简体) ), @ai-matrx/meet必须
因此,当主机拥有一个时,骑乘主机的经理,只用一个应用程序上载它自己的
无线 :
const hostProvidesRealtime = useIsRealtimeProviderMounted();
if (hostProvidesRealtime) return <Runtime {...props} />;
return <RealtimeProvider client={client}><Runtime {...props} /></RealtimeProvider>;
询问 useIsRealtimeProviderMounted()永远 useRealtimeManager() !== null - 经理是
当挂载的提供者等待它的 Supabase 客户端时, 和一个猜测到的软件包, 也是无效的
从它上起一个供货商 然后撕裂它, 和它下面的每一个通道
即时主持人的客户端到来.
Redux 中间软件、 tunks 和其他非反应所有者
规则 第4条 偏好使用 Redux 中间软件作为特性的订阅所有者——组件效果
重新订阅身份churn和乘以跨挂载的频道。 中间软件无法调用
钩子,所以提供者 发表 它的经理和非反应所有者阅读它。 这是一个
指向供应商所建的 ONE 管理器; 在中间软件中构建自己的是第二个
写分类账,你每写一个乐观的字 就会归类为 remote 费用a
补补.
import { subscribeToRealtimeManager } from "@ai-matrx/realtime";
// Opens on arrival, closes on departure, RE-OPENS on the new manager when the
// provider rebuilds one (the signed-in user changed). Returns a stop function.
const stop = subscribeToRealtimeManager(() => ({
topic: notesChannel.topic({ userId: currentUserId() }),
postgresChanges: [ /* … */ ],
onBackfill: () => store.dispatch(fetchNotes()),
}));
currentRealtimeManager() 财务报告和审定财务报表 onRealtimeManagerChange(listener) 是下面的原始人.
阅读 currentRealtimeManager() 一次 模块负载几乎总是错误:是 null
在裁决之前,管理者将被重建
当签名用户更改时。 偏爱 subscribeToRealtimeManager。 。 。 .
光谱是 工厂 因此所有者会抓取当前用户 id / dispatch 开放时间
而不是在舱载.
私人频道(数据库广播)
授权反对的主题 realtime.messages RLS - 如何实现 realtime.broadcast_changes()
从 Postgres 触发器只向有资格的用户发送行—— 声明 private数字 :
manager.open({
topic: schedulerChannel.topic({ userId }),
private: true,
broadcast: [{ event: "UPDATE", onMessage: ({ data }) => apply(data) }],
onBackfill: () => refetchSchedules(),
});
软件包创建频道 config.private: true 财务报告和审定财务报表 授权套接字
(单位:千美元)client.realtime.setAuth()签署前。 第二步不是可选的,而是
整个地表最安静的失败:没有它,就拒绝加入,就是频道
永远得不到任何东西, 它报告自己完全健康。 客户端不能
授权获得 channel.private.unsupported; 不附加的符号得到
channel.private.auth-failed。 。 。 .
自动保存共存
在共享分类账上注册写入 在此之前 乐观的更新,和回声是 一个真正的合作者写着:
const manager = useRealtimeManager();
const ticket = manager.ledger.begin({
table: "workbench.notes",
id: note.id,
fingerprint: draft.content,
});
try {
const saved = await saveNote(draft);
manager.ledger.settle(ticket, { updatedAt: saved.updated_at, fingerprint: saved.content });
} catch (error) {
manager.ledger.abandon(ticket);
throw error;
}
ledger.classify(...) 返回时 own-echo | remote | stale 加号 localWritePending,所以一个
乐观的写作,它的回声,和真正的远程写作都是可以区分的.
出席和打字
const { others } = usePresence({
topic: roomChannel.topic({ roomId }),
presence: { state: { userId, name } },
});
const { typists, onKeystroke, stopTyping } = useTyping({
topic: chatChannel.topic({ conversationId }),
identity: { userId, name },
});
坠机客户端不能离开幽灵参与者(心跳+本地时长过期) "X在打字..."不能坚持(打字是续租,从来不是等待停站的锁) 可能永远到不了的信息.
反应原生/博览会
import { NativeRealtimeProvider, useChannel } from "@ai-matrx/realtime/native";
<NativeRealtimeProvider client={supabase} actorId={userId}>
{children}
</NativeRealtimeProvider>;
相同的钩子,与 AppState 已接通的接合:背景 = 前景调节
和复相会. react-native 是一个可选的对等点;网络条目从不加载.
诊断
import { realtimeDiagnostics } from "@ai-matrx/realtime";
// or: const snapshot = useRealtimeDiagnostics();
直播频道,订阅状态,事件/分钟,以及从未有过的数字:
suppressedEcho, (中文(简体) ), deduped, (中文(简体) ), stale, (中文(简体) ), queueDepth, (中文(简体) ), queueDropped, (中文(简体) ), handlerErrors, (中文(简体) ),
backfills。实时错误的意思是 观察,没有猜测.
入境点
| 条目 | 包含 | "use client" |
|---|---|---|
@ai-matrx/realtime | 无框架核心:管理器,写出分类账,话题,存在/打字运行时间,队列,后退,脱发,诊断 | 否(不受RSC、Redux中间软件、工人、节点的保险) |
@ai-matrx/realtime/react | RealtimeProvider, (中文(简体) ), useChannel, (中文(简体) ), usePresence, (中文(简体) ), useTyping, (中文(简体) ), useRealtimeDiagnostics, (中文(简体) ), useRealtimeHealth - 加上整个核心 | 对 |
@ai-matrx/realtime/native | NativeRealtimeProvider, (中文(简体) ), createReactNativeEnvironment 加上整个反应绑定 | 对 |
零运行时间依赖关系. react 财务报告和审定财务报表 react-native 是可选的同龄人。 包裹从来没有
进口 @supabase/supabase-js: 客户端在结构上被接受, 以及编译时间证明
人民群众群众 src/core/supabase-shape.test.ts。 。 。 .
移动手卷频道
- 声明特性的命名空间;删除手写的主题字符串.
- 替换
.channel(...)块为useChannel(或manager.open在中间软件. - 将读取的快取移动到
onBackfill如果模块没有,这就是你的错误 修复. - 删除模块本身的回声/dedup/后退代码. 将第二个副本保存在包边 是命名失败,而不是安全选项.
uniqueChannelTopic住在这里.@ai-matrx/data/db其再出口(数据为0.6.3) 现有呼叫站点继续工作,但移徙功能完全停止呼叫:a 命名空格名称 频道和管理者按主题语义决定有线主题 第7条之分. 一个手卷广播或现场频道正在呼叫uniqueChannelTopic自己却在自己的房间里分道扬镳 - 这是你正在修理的虫子,不是 行为要保护.
许可证
麻省理工学院