@ datafn/ 客户端
DataFn 的离线第一反应客户端. 提供流利表和 KV API, 用于 UI 绑定的反应信号, 浏览器拥有的索引DB 存储, 本地支持的核心数据存储通过 @datafn/swift-bridge,双向同步,一个事件总线,交易,插件,以及多用户数据隔离.
安装
npm install @datafn/client @datafn/core
对于想要斯威夫特拥有的持久和同步的苹果Webview主机:
npm install @datafn/client @datafn/core @datafn/swift-bridge
地形矩阵
| 地形学 | 地方持久性 | 远距离持久性 | 同步所有者 |
|---|---|---|---|
| 浏览器拥有 | 索引数据库 | DataFn 服务器 | Java 脚本 |
| 本地支持的数据Fn 服务器 | 核心数据 | DataFn 服务器 | 挥发 |
| 本地备份 CloudKit | 核心数据 | CloudKit 私人数据库 | 挥发 |
原生后盾模式是明确的. 如果桥没了,它不会悄悄倒回索引数据库.
特征
| 特性 | 说明 |
|---|---|
| 流利的表格 API | client.table("resource") ——范围查询,变异,信号 |
| 反应信号 | 当数据变化时自动更新的实时查询 |
| KV 存储 | 包含信号支持的内置密钥值 API(client.kv(中文(简体) ) |
| 离线存储 | 有基于更动日志的离线变异的索引DB和内存适配器 |
| 同步 | 克隆、拉、推、克隆、调和——与水分计划 |
| 仅线下模式 | sync.mode: "local-only" - 不需要服务器 |
| 活动总线 | 用于突变和同步生命周期的全球事件流 |
| 交易 | 跨资源原子多步骤行动 |
| 插件系统 | 截取询问、突变和自定义逻辑同步 |
| 日期编码c | 日期字段的自动序列化/分页 |
| 多用户隔离 | 用户索引数据库 authContext + 储存厂 |
| 扩展适配器 | 浏览器扩展支持通过 remoteAdapter 有远程订阅 |
| 类型安全 | 从您的计划中得出完整的脚本推论 |
快速启动
import { createDatafnClient, IndexedDbStorageAdapter } from "@datafn/client";
import type { DatafnSchema } from "@datafn/core";
const schema: DatafnSchema = {
resources: [
{
name: "tasks",
version: 1,
fields: [
{ name: "id", type: "string", required: true, unique: true },
{ name: "title", type: "string", required: true },
{ name: "completed", type: "boolean", required: true, default: false },
],
},
],
};
const client = createDatafnClient({
schema,
clientId: "device-" + crypto.randomUUID(),
storage: new IndexedDbStorageAdapter("my-app-db"),
sync: {
offlinability: true,
remote: "http://localhost:3000/datafn",
},
});
// Start sync (clone + pull + push engine)
await client.sync.start();
// Insert a record
await client.table("tasks").mutate({
operation: "insert",
record: { title: "Hello DataFn", completed: false },
});
// Create a reactive signal
const signal = client.table("tasks").signal({
filters: { completed: false },
sort: ["-createdAt"],
});
signal.subscribe((result) => {
console.log("Active tasks:", result.data);
});
本地支持的苹果网络视频模式
当同一网页应用程序被嵌入 SwiftUI 主机中时, 配置客户端 :
import { createDatafnClient } from "@datafn/client";
import {
createNativeBackedRemoteAdapter,
createNativeBackedSearchProvider,
createNativeBackedStorageAdapter,
createNativeSyncController,
createWKWebViewBridgeBus,
} from "@datafn/swift-bridge";
const bus = createWKWebViewBridgeBus({ handlerName: "datafn" });
const client = createDatafnClient({
schema,
clientId: "apple-webview-device",
namespace: "org-1:user-1",
storage: createNativeBackedStorageAdapter(bus),
searchProvider: createNativeBackedSearchProvider(bus),
sync: {
owner: "native",
mode: "sync",
offlinability: true,
remoteAdapter: createNativeBackedRemoteAdapter(bus),
native: {
syncController: createNativeSyncController(bus),
remoteMode: "datafn-server",
expectedSchemaHash: "todo-app-example-v1",
failIfUnavailable: true,
remoteProfile: "default",
},
},
});
对于 CloudKit 支持的个人应用程序, 更改 remoteMode 改为 "icloud"。在两种本地支持的模式中:
- 斯威夫特拥有持久性和同步性.
- 斯威夫特还拥有由SearchFn支撑的本地索引.
- Java 脚本
SyncEngine必须保持不活动. - DataFn 如果桥不可用, 则在启动持久性前必须失败 .
- 索引DB不得用作倒置的持久性或搜索-索引层.
- CloudKit只同步记录. 搜索索引文件仍为每个设备的本地状态 .
客户端配置
创建 DatafnClient( 配置)
interface DatafnClientConfig<S extends DatafnSchema> {
/** Your DataFn schema definition */
schema: S;
/** Stable client/device identifier — required for offline + idempotency */
clientId: string;
/** Sync configuration (see below) */
sync?: DatafnSyncConfig;
/**
* Local persistence adapter.
* Can be a direct adapter instance or a factory function for multi-user isolation.
*/
storage?: DatafnStorageAdapter | DatafnStorageFactory;
/** Auth context for multi-user/multi-tenant data isolation */
authContext?: AuthContext | AuthContextProvider;
/** Optional plugins for hook execution */
plugins?: DatafnPlugin[];
/** Custom timestamp function (for testing) */
getTimestamp?: () => number;
/**
* Custom ID generator for insert operations.
* Default: `${idPrefix || resource}:${crypto.randomUUID()}`
*/
generateId?: (params: { resource: string; idPrefix?: string }) => string;
}
Datafn 同步配置
interface DatafnSyncConfig {
/**
* Explicit mode selection.
* - "sync": requires remote or remoteAdapter
* - "local-only": no server required; all tables start as "ready"
*/
mode?: "sync" | "local-only";
/** Enable offline support (requires storage) */
offlinability?: boolean;
/** Remote server URL for the default HTTP transport */
remote?: string;
/** Injected remote adapter (takes precedence over remote URL) */
remoteAdapter?: DatafnRemoteAdapter;
/** Enable WebSocket for real-time server-push updates */
ws?: boolean;
/** WebSocket URL (derived from remote if omitted) */
wsUrl?: string;
/** Push engine: interval between batches (ms). Default 2000. */
pushInterval?: number;
/** Push engine: records per batch. Default 100. */
pushBatchSize?: number;
/** Push engine: max retries per mutation. Default 3. */
pushMaxRetries?: number;
/** Hydration plan for large datasets */
hydration?: {
/** Resources that MUST be cloned before the app is considered "ready" */
bootResources?: string[];
/** Resources that hydrate in the background after boot */
backgroundResources?: string[];
/** Per-resource clone page size (or a single number for all) */
clonePageSize?: number | Record<string, number>;
};
}
表 API 数据
这个 client.table(name) 方法返回特定资源的范围控件。 您也可以作为属性访问表格 : client.tasks 等同为 client.table("tasks")。 。 。 .
const tasks = client.table("tasks");
// or equivalently:
const tasks = client.tasks;
表.query(碎片)
执行此资源的查询范围 .
const result = await tasks.query({
select: ["id", "title", "completed"],
filters: { completed: false },
sort: ["-createdAt"],
limit: 20,
});
// result.data = [{ id: "task:...", title: "...", completed: false }, ...]
查询特性 :
select编号 :omit- 外地选择filters- 运算符:eq, (中文(简体) ),ne, (中文(简体) ),gt, (中文(简体) ),gte, (中文(简体) ),lt, (中文(简体) ),lte, (中文(简体) ),like, (中文(简体) ),ilike, (中文(简体) ),is_null, (中文(简体) ),is_not_null, (中文(简体) ),in, (中文(简体) ),nin, (中文(简体) ),contains- 逻辑组 :
$and, (中文(简体) ),$or sort——多领域:["name", "-createdAt"](前缀)-= 下降数)limit编号 :offset——以抵消为基础的平面图cursor——以光标为主的平面({ after: {...} }(中文(简体) )count——只计算返回数groupBy编号 :aggregations编号 :having——汇总查询search——全文搜索
搜索( 提供备份, 本地第一)
DataFn 搜索在 searchProvider 已配置。 在同步模式下,在水合准备后搜索是本地第一.
地形特有搜索所有权 :
- 浏览器拥有模式: 使用 JavaScript 搜索Fn 提供者, 通常
@searchfn/datafn-provider与@searchfn/adapter-indexeddb。 。 。 . - 本地支持的 Apple WebView 模式: 使用
createNativeBackedSearchProvider(bus)因此Swift针对共享本地搜索Fn后端执行搜索. - 原生后卫CloudKit模式:使用同一种原生的SearchFn后端,但CloudKit同步只记录;索引文件每设备重建并在当地维护.
table.query() 搜索块选项 :
const result = await tasks.query({
search: {
query: "test",
prefix: true,
fuzzy: 0.2,
fieldBoosts: { title: 2, description: 1 },
},
});
具有明确来源路径的跨资源搜索 :
const result = await client.search({
query: "test",
resources: ["tasks", "projects"],
prefix: true,
fuzzy: 0.2,
fieldBoosts: { title: 2, name: 1 },
source: "auto", // auto | local | remote
});
source 语义 :
auto(默认): 本地先; 如果本地提供者路径不可用, 返回到远程 .local:强迫本地提供者执行.remote: 强制远程/datafn/search执行.
如果找不到请求的来源, DataFn 返回 DFQL_UNSUPPORTED。 。 。 .
MiniSearch-仅限插件模式仍然支持兼容性,但提供者支持的模式是推荐的路径.
表格. mutate(碎片)
执行此资源的突变范围 .
// Insert
await tasks.mutate({
operation: "insert",
record: { title: "New task", completed: false },
});
// Merge (partial update)
await tasks.mutate({
operation: "merge",
id: "task:abc",
record: { completed: true },
});
// Replace (full update)
await tasks.mutate({
operation: "replace",
id: "task:abc",
record: { title: "Updated", completed: true },
});
// Delete
await tasks.mutate({
operation: "delete",
id: "task:abc",
});
变异操作 :
| 运行 | 说明 |
|---|---|
insert | 创建新记录 |
merge | 部分更新( 仅指定字段) |
replace | 完整更新( 替换整个记录) |
delete | 删除记录 |
关系业务 (使用时 client.mutate() 配备全部资源/版本:
| 运行 | 说明 |
|---|---|
relate | 在记录之间建立关系 |
unrelate | 删除记录之间的关系 |
modifyRelation | 更新关系元数据 |
// Tag a todo with a category (many-many relation)
await client.mutate({
resource: "todos",
version: 1,
operation: "relate",
id: "todo:1",
relation: "tags",
targetId: "cat:work",
});
// Remove a tag
await client.mutate({
resource: "todos",
version: 1,
operation: "unrelate",
id: "todo:1",
relation: "tags",
targetId: "cat:work",
});
高级突变特征:
- 目 录数字 :
clientId+ 键mutationId用于复制 - 优化货币数字 :
if警卫 预防冲突 - A. 背景情况: 将任意上下文数据传递给插件和事件
表格. signal( 碎片)
创建反应信号——当数据变化时自动更新的活查询.
const activeTasks = tasks.signal({
filters: { completed: false },
sort: ["-createdAt"],
});
// Get current value
console.log(activeTasks.get());
// Subscribe to changes
const unsub = activeTasks.subscribe((result) => {
console.log("Tasks:", result.data);
console.log("Loading:", result.loading);
});
// Check states
activeTasks.loading; // true while initial fetch is in progress
activeTasks.error; // non-null if last fetch failed
activeTasks.refreshing; // true while background refresh is in progress
信号特性 :
- 懒取: 只在第一次订阅时加载数据
- 自动更新:突变影响查询脚印时重跑
- 跳出分批:多次快速突变触发单一刷新
- 缓存: 具有相同查询的信号共享一个缓存实例( 通过)
dfqlKey(中文(简体) )
表格. 订阅( 处理器, 过滤器 ?)
只订阅此资源的事件 .
tasks.subscribe((event) => {
console.log(`${event.action} on ${event.resource}:`, event.ids);
});
韩国API
内置的密钥值商店提供了一个与您输入的资源同步的无计划存储层. 透过 client.kv。 。 。 .
// Set a value
await client.kv.set("user:theme", "dark");
// Get a value
const theme = await client.kv.get<string>("user:theme");
// → "dark"
// Merge into an object value
await client.kv.set("user:prefs", { fontSize: 14, lang: "en" });
await client.kv.merge("user:prefs", { fontSize: 16 });
// → { fontSize: 16, lang: "en" }
// Delete a key
await client.kv.delete("user:theme");
// Reactive signal for a key
const themeSignal = client.kv.signal<string>("user:theme", {
defaultValue: "dark",
});
themeSignal.subscribe((value) => {
document.body.className = value; // Updates reactively
});
KV API 参考文献
| 方法 | 签署 | 说明 |
|---|---|---|
get | get<T>(key): Promise<T | null> | 读取一个值 |
set | set<T>(key, value, params?): Promise<Result> | 写入一个值( 替换语义) |
merge | merge(key, patch, params?): Promise<Result> | 浅入已存在的对象 |
delete | delete(key, params?): Promise<Result> | 删除密钥 |
signal | signal<T>(key, options?): DatafnSignal<T> | 密钥的响应信号 |
KV 数据存储在内建 kv 和任何其他资源一样参与同步(clone/pull/push).
仅线下模式
在完全没有服务器的情况下运行客户端 。 所有数据只存在于本地存储中.
const client = createDatafnClient({
schema,
clientId: "local-device",
storage: new IndexedDbStorageAdapter("my-app-local"),
sync: {
mode: "local-only",
},
});
// No sync.start() needed — all tables are immediately "ready"
// Queries and mutations work against local storage
await client.table("tasks").mutate({
operation: "insert",
record: { title: "Offline task" },
});
当在 local-only 模式 :
- 所有资源水分状态设定为
"ready"马上 - 只对本地存储执行查询
- 对本地存储的突变表示乐观
- 没有网络通话
- 同步外观方法( E)
clone, (中文(简体) ),pull, (中文(简体) ),push(如果调用,则丢出)
同步
同步外观
client.sync.seed(payload) // Seed data to server
client.sync.clone(payload) // Full data download
client.sync.pull(payload) // Incremental sync (cursor-based)
client.sync.push(payload) // Upload local mutations
client.sync.cloneUp(options?) // Upload local data to server
同步引擎
何时 offlinability 真实,同步引擎管理整个生命周期:
await client.sync.start(); // Start sync engine (clone → pull loop → push loop)
client.sync.stop(); // Stop sync engine
await client.sync.pullNow(); // Trigger immediate pull
await client.sync.cloneNow(); // Trigger immediate clone
await client.sync.reconcileNow(); // Trigger reconcile
同步引擎行为 :
- 打开
start(): 克隆启动资源, 然后开始拉和推间隔 - 调取可见度变化: 标签可见时重排
- 按下批量:排队突变并分批地推到
pushInterval - 重试: 重试失败后再重试
pushMaxRetries时间
水分国
每个资源跟踪其水分状态:
| 状态 | 说明 |
|---|---|
notStarted | 尚未复制数据 |
hydrating | 克隆人正在进行中 |
ready | 有数据可供查询 |
配置启动对背景资源来控制应用准备 :
sync: {
hydration: {
bootResources: ["tasks", "projects"], // Must clone before app is ready
backgroundResources: ["audit_log"], // Hydrates after boot
clonePageSize: { tasks: 500, audit_log: 100 },
},
}
克隆
上传本地数据到服务器( 例如下线工作后):
const result = await client.sync.cloneUp({
resources: ["tasks"], // Which resources to upload (default: all)
includeManyMany: true, // Upload join rows too
recordOperation: "merge", // "merge" | "replace" | "insert"
batchSize: 100, // Records per batch
maxRetries: 3, // Retries per batch
failFast: false, // Stop on first error?
clearChangelogOnSuccess: true, // Drain changelog after upload
setGlobalCursorOnSuccess: true,// Update cursors
pullAfter: true, // Pull new data after upload
});
console.log(result.uploadedCount);
活动总线
订阅全局事件或过滤器, 按资源, 类型, 动作等等 .
// Global subscription
const unsub = client.subscribe((event) => {
console.log(event.type, event.resource, event.ids);
});
// Filtered subscription
const unsub2 = client.subscribe(
(event) => console.log("Task mutated:", event),
{
type: "mutation_applied",
resource: "tasks",
action: ["insert", "merge"],
},
);
事件过滤器
type EventFilter = {
type?: string | string[];
resource?: string | string[];
ids?: string | string[];
mutationId?: string | string[];
action?: string | string[];
fields?: string | string[];
contextKeys?: string[];
context?: Record<string, unknown>;
};
匹配过滤器
用于检查事件在程序上是否匹配过滤器的功能 :
import { matchesFilter } from "@datafn/client";
if (matchesFilter(event, { resource: "tasks", type: "mutation_applied" })) {
// handle
}
交易
执行原子多步骤操作 :
const result = await client.transact({
transactionId: "tx-complete-all",
atomic: true,
steps: [
{
query: {
resource: "tasks",
version: 1,
select: ["id"],
filters: { completed: false },
},
},
{
mutation: {
resource: "tasks",
version: 1,
operation: "merge",
id: "task:1",
record: { completed: true },
},
},
{
mutation: {
resource: "tasks",
version: 1,
operation: "delete",
id: "task:2",
},
},
],
});
存储适配器
已索引的DbStorageAdapter
由IndexedDB支持的持续浏览器存储. 支持多用户隔离.
import { IndexedDbStorageAdapter } from "@datafn/client";
// Simple usage
const storage = new IndexedDbStorageAdapter("my-app-db");
// Multi-user isolation
const storage = IndexedDbStorageAdapter.createForUser(
"my-app-db",
userId,
tenantId, // optional
);
// Creates database: "my-app-db_tenant-456_user-123"
内存图集Adapter
用于测试的内存存储——数据丢失于页面刷新.
import { MemoryStorageAdapter } from "@datafn/client";
const storage = new MemoryStorageAdapter();
DatafnStorageAdapter 接口
为自定义存储后端执行此接口 :
interface DatafnStorageAdapter {
// Records
getRecord(resource: string, id: string): Promise<Record<string, unknown> | null>;
listRecords(resource: string): Promise<Record<string, unknown>[]>;
upsertRecord(resource: string, record: Record<string, unknown>): Promise<void>;
deleteRecord(resource: string, id: string): Promise<void>;
findRecords(resource: string, field: string, value: unknown): Promise<Record<string, unknown>[]>;
countRecords(resource: string): Promise<number>;
// Join rows (many-many relations)
listJoinRows(relationKey: string): Promise<Array<Record<string, unknown>>>;
getJoinRows(relationKey: string, fromId: string): Promise<Array<Record<string, unknown>>>;
getJoinRowsInverse(relationKey: string, toId: string): Promise<Array<Record<string, unknown>>>;
upsertJoinRow(relationKey: string, row: Record<string, unknown>): Promise<void>;
setJoinRows(relationKey: string, rows: Array<Record<string, unknown>>): Promise<void>;
deleteJoinRow(relationKey: string, from: string, to: string): Promise<void>;
countJoinRows(relationKey: string): Promise<number>;
// Sync state
getCursor(resource: string): Promise<string | null>;
setCursor(resource: string, cursor: string | null): Promise<void>;
getHydrationState(resource: string): Promise<DatafnHydrationState>;
setHydrationState(resource: string, state: DatafnHydrationState): Promise<void>;
// Offline changelog
changelogAppend(entry: Omit<DatafnChangelogEntry, "seq">): Promise<DatafnChangelogEntry>;
changelogList(options?: { limit?: number }): Promise<DatafnChangelogEntry[]>;
changelogAck(options: { throughSeq: number }): Promise<void>;
}
多用途/多用途隔离
在单独的索引数据库中孤立每个用户的数据.
备选案文1:提供(建议)
import { createDatafnClient, IndexedDbStorageAdapter } from "@datafn/client";
const client = createDatafnClient({
schema,
clientId: "device-uuid",
authContext: authClient.contextProvider, // implements { getContext(): AuthContext }
storage: (ctx) =>
IndexedDbStorageAdapter.createForUser("my-app", ctx.userId, ctx.tenantId),
sync: { remote: "http://localhost:3000/datafn" },
});
备选案文2:直接认证
const client = createDatafnClient({
schema,
clientId: "device-uuid",
authContext: { userId: "user-123", tenantId: "tenant-456" },
storage: (ctx) =>
IndexedDbStorageAdapter.createForUser("my-app", ctx.userId, ctx.tenantId),
sync: { remote: "http://localhost:3000/datafn" },
});
当用户登录和另一个日志输入时,创建新的客户端实例. 每个用户的数据仍被隔离在自己的索引数据库中.
日期编码c
自动序列化和解析 date 字段.
import {
serializeDateFields,
parseDateFields,
parseQueryResultDates,
} from "@datafn/client";
// Serialize Date objects to timestamps for mutations
const serialized = serializeDateFields(schema, "tasks", {
title: "Hello",
createdAt: new Date(),
});
// Parse timestamps back to Date objects
const parsed = parseDateFields(schema, "tasks", {
title: "Hello",
createdAt: 1707000000000,
});
// Parse all date fields in a query result
const result = parseQueryResultDates(schema, "tasks", queryResult);
插件
扩展客户端行为,以插件来拦截查询,突变,并同步.
import type { DatafnPlugin } from "@datafn/core";
const loggingPlugin: DatafnPlugin = {
name: "logger",
runsOn: ["client"],
afterMutation(ctx, mutation, result) {
console.log("Mutation:", mutation, "Result:", result);
},
afterSync(ctx, phase, payload, result) {
console.log(`Sync ${phase}:`, result);
},
};
const client = createDatafnClient({
schema,
clientId: "...",
plugins: [loggingPlugin],
// ...
});
远程适应器
当您提供时使用默认的 HTTP 传输 sync.remote。对于自定义运输(仅WebSocket、浏览器扩展等),执行 DatafnRemoteAdapter数字 :
interface DatafnRemoteAdapter {
query(q: unknown): Promise<unknown>;
mutation(m: unknown): Promise<unknown>;
transact(t: unknown): Promise<unknown>;
seed(payload: unknown): Promise<unknown>;
clone(payload: unknown): Promise<unknown>;
pull(payload: unknown): Promise<unknown>;
push(payload: unknown): Promise<unknown>;
reconcile(payload: unknown): Promise<unknown>;
}
扩展适配器
对于浏览器扩展,远程适配器可以包含事件订阅支持:
const client = createDatafnClient({
schema,
clientId: "extension-popup",
sync: {
remoteAdapter: {
...transportMethods,
onEvent(handler) { /* wire inbound events */ },
subscribeRemote(filter) { /* register subscription */ },
unsubscribeRemote(id) { /* remove subscription */ },
},
},
});
出口
// Client factory and types
export { createDatafnClient, type DatafnClient, type DatafnClientConfig, type DatafnRemoteAdapter }
// Table API
export { type DatafnTable }
// Event system
export { EventBus, type EventHandler }
export { matchesFilter, type EventFilter }
// Storage
export { type DatafnStorageAdapter, type DatafnStorageFactory }
export { type DatafnHydrationState, type DatafnChangelogEntry }
export { MemoryStorageAdapter }
export { IndexedDbStorageAdapter }
// KV API
export type { DatafnKvApi }
export { kvId, KV_RESOURCE_NAME }
// CloneUp
export type { CloneUpOptions, CloneUpResult }
// Date Codec
export { serializeDateFields, parseDateFields, parseQueryResultDates }
// Auth (re-exported from @superfunctions/auth)
export type { AuthContext, AuthContextProvider }
// Errors
export { type DatafnClientError, createClientError }
export { unwrapRemoteSuccess }
许可证
麻省理工学院