feat(0.2.1): M3 前端 PowerSync 基础 (schema/connector/db) + 依赖; fix storage sslmode

This commit is contained in:
jif2.zhang
2026-10-26 00:07:33 +08:00
parent a386660833
commit a4af607c41
6 changed files with 246 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
// PowerSync 后端连接器: 凭证获取 + 上传队列落地 (0.2.1)
// 契约见 server/routes/powersync.js: POST /api/powersync/upload { ops: [{ id, table, op, uuid, data }] }
import { PowerSyncBackendConnector, UpdateType } from '@powersync/web'
import { api, getToken } from '../../api/client.js'
export class IBoardConnector extends PowerSyncBackendConnector {
async fetchCredentials() {
// 未登录: 返回 null 让 SDK 保持断开 (登录后由页面触发 connect)
if (!getToken()) return null
const { token, endpoint } = await api('/powersync/token', { method: 'POST' })
return { endpoint: endpoint || '', token }
}
async invalidateCredentials() {
// 凭证不缓存, 每次向业务后端现取
}
async uploadData(database) {
const batch = await database.getCrudBatch()
if (!batch) return
// 本地行的 id 即为 uuid (客户端创建时生成), 服务端按 uuid 幂等 upsert
const ops = batch.crud.map((op, i) => ({
id: String(i),
table: op.table,
op: op.op === UpdateType.DELETE ? 'DELETE' : 'PUT',
uuid: op.id,
data: op.opData || {},
}))
let res
try {
res = await api('/powersync/upload', { method: 'POST', body: { ops } })
} catch (e) {
// 网络失败/401: 抛出让 SDK 稍后重试本批
throw e
}
const failed = (res?.results || []).filter((r) => !r.ok)
if (failed.length) {
// 单条业务错误: 记录并放行 (重试也无法成功, 避免队列卡死); 服务端已尽力落地其余 op
console.error('[powersync] upload 部分失败:', failed)
}
await batch.complete()
}
}
+37
View File
@@ -0,0 +1,37 @@
// PowerSync 本地库单例 (0.2.1 离线编辑)
// 页面用法: usePowerSyncQuery 等见 src/lib/powersync/react.jsx; 写入用 db.execute/writeTransaction
import { PowerSyncDatabase } from '@powersync/web'
import { AppSchema } from './schema.js'
import { IBoardConnector } from './connector.js'
let dbInstance = null
export function getPowersyncDb() {
if (!dbInstance) {
dbInstance = new PowerSyncDatabase({
schema: AppSchema,
database: {
// 默认 IDBBatchAtomicVFS (IndexedDB), 兼容性最广; WebView2 (Chromium) 后续可切 OPFS 加速
dbFilename: 'iboard-powersync.db',
},
})
}
return dbInstance
}
/** 登录后调用: 建立同步连接 (幂等) */
export async function connectPowersync() {
const db = getPowersyncDb()
db.connect(new IBoardConnector())
return db
}
/** 退出登录时调用: 断开同步并清空本地库 */
export async function disconnectAndClearPowersync() {
if (!dbInstance) return
try {
await dbInstance.disconnectAndClear()
} catch {
/* ignore */
}
}
+94
View File
@@ -0,0 +1,94 @@
// PowerSync 客户端 schema (0.2.1 离线编辑)
// 表名/列名与 deploy/0.2.1/powersync/sync-config.yaml 的输出严格一致 (PascalCase + text 型 id 由 SDK 自建)
// 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md
import { column, Schema, Table } from '@powersync/web'
// 时间/JSON 列在同步流里以文本落地, 本地按字符串读写 (JSON 列为 JSON 字符串)
const project = new Table(
{
uuid: column.text,
code: column.text,
name: column.text,
ownerId: column.text,
isPersonal: column.integer,
archived: column.integer,
notes: column.text,
taskKinds: column.text,
icon: column.text,
createdAt: column.text,
updatedAt: column.text,
},
{ indexes: { code: ['code'] } },
)
const projectMember = new Table({
uuid: column.text,
projectId: column.text,
userId: column.text,
permission: column.text,
createdAt: column.text,
})
const boardColumn = new Table(
{
uuid: column.text,
projectId: column.text,
name: column.text,
sortOrder: column.integer,
boundStatus: column.text,
retention: column.text,
createdAt: column.text,
updatedAt: column.text,
},
{ indexes: { project: ['projectId'] } },
)
const task = new Table(
{
uuid: column.text,
projectId: column.text,
columnId: column.text,
parentId: column.text,
title: column.text,
description: column.text,
kind: column.text,
markDate: column.text,
status: column.text,
doneAt: column.text,
assigneeId: column.text,
creatorId: column.text,
extras: column.text,
sortOrder: column.integer,
number: column.integer,
createdAt: column.text,
updatedAt: column.text,
},
{ indexes: { project: ['projectId'], column: ['columnId'], number: ['projectId', 'number'] } },
)
const taskMember = new Table({
uuid: column.text,
taskId: column.text,
userId: column.text,
})
const taskTag = new Table({
uuid: column.text,
taskId: column.text,
name: column.text,
})
const user = new Table({
name: column.text,
email: column.text,
})
export const AppSchema = new Schema({
Project: project,
ProjectMember: projectMember,
BoardColumn: boardColumn,
Task: task,
TaskMember: taskMember,
TaskTag: taskTag,
User: user,
})