From a4af607c419eefe35a6465de336cd896f0a5936f Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Mon, 26 Oct 2026 00:07:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(0.2.1):=20M3=20=E5=89=8D=E7=AB=AF=20PowerS?= =?UTF-8?q?ync=20=E5=9F=BA=E7=A1=80=20(schema/connector/db)=20+=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96;=20fix=20storage=20sslmode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/0.2.1/powersync/service.yaml | 1 + package-lock.json | 67 ++++++++++++++++++++ package.json | 2 + src/lib/powersync/connector.js | 45 ++++++++++++++ src/lib/powersync/db.js | 37 ++++++++++++ src/lib/powersync/schema.js | 94 +++++++++++++++++++++++++++++ 6 files changed, 246 insertions(+) create mode 100644 src/lib/powersync/connector.js create mode 100644 src/lib/powersync/db.js create mode 100644 src/lib/powersync/schema.js diff --git a/deploy/0.2.1/powersync/service.yaml b/deploy/0.2.1/powersync/service.yaml index 18d9892..77a21b6 100644 --- a/deploy/0.2.1/powersync/service.yaml +++ b/deploy/0.2.1/powersync/service.yaml @@ -13,6 +13,7 @@ replication: storage: type: postgresql uri: postgresql://powersync_storage:!env PS_STORAGE_PASSWORD@pg:5432/iboard021 + sslmode: disable port: 8080 diff --git a/package-lock.json b/package-lock.json index ca2d227..2d9f350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "iboard", "version": "0.2.0", "dependencies": { + "@powersync/react": "^1.10.0", + "@powersync/web": "^1.39.1", "@prisma/adapter-pg": "^7.9.1", "@prisma/client": "^7.7.0", "@zythum02/mijia-api": "^0.1.4", @@ -1187,6 +1189,13 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@journeyapps/wa-sqlite": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@journeyapps/wa-sqlite/-/wa-sqlite-1.7.2.tgz", + "integrity": "sha512-EVRMzqHtHgJBFcTGOgQ5/YlxrYFSXLFFztC5yo6+jQx2mqFR+a1kkLV4IirquiBzIonjmYXfBtvZ26fmI8r9Nw==", + "hasInstallScript": true, + "peer": true + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1615,6 +1624,52 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@powersync/common": { + "version": "1.57.3", + "resolved": "https://registry.npmjs.org/@powersync/common/-/common-1.57.3.tgz", + "integrity": "sha512-gzXcdfO1EnwXNBITDbnx7oN23d91HjoumS/XzPmVZEwV6S/IBcao/Unv4+aQDvijQVzTtXI+jTl98i9v+TrODQ==", + "license": "Apache-2.0", + "dependencies": { + "js-logger": "^1.6.1" + } + }, + "node_modules/@powersync/react": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@powersync/react/-/react-1.10.0.tgz", + "integrity": "sha512-orM29qocbF8zYTBM27j9nPNztyplpbNVaw8djpqxl8N2wESqHTbHOKpatg+Fa5aofgjzmrkiGwevJvCbiWYYWQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@powersync/common": "^1.51.0", + "react": "*" + } + }, + "node_modules/@powersync/web": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@powersync/web/-/web-1.39.1.tgz", + "integrity": "sha512-mu01OE3sQrulRgvDOWw1xRbRbPhJMOfz5sIVIQ2lENGDkqgWdlRhpkHVfeVi80tCZoP/AFYkQAFRszCgzqFUxQ==", + "license": "Apache-2.0", + "dependencies": { + "@powersync/common": "1.57.3", + "comlink": "^4.4.2", + "commander": "^12.1.0" + }, + "bin": { + "powersync-web": "bin/powersync.cjs" + }, + "peerDependencies": { + "@journeyapps/wa-sqlite": "^1.5.0", + "@powersync/common": "^1.57.3" + } + }, + "node_modules/@powersync/web/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz", @@ -3539,6 +3594,12 @@ "node": ">= 0.8" } }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -4946,6 +5007,12 @@ "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", "license": "MIT" }, + "node_modules/js-logger": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/js-logger/-/js-logger-1.6.1.tgz", + "integrity": "sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 00c7ffe..ed2d713 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "publish": "bash scripts/wsl/build-apk.sh release" }, "dependencies": { + "@powersync/react": "^1.10.0", + "@powersync/web": "^1.39.1", "@prisma/adapter-pg": "^7.9.1", "@prisma/client": "^7.7.0", "@zythum02/mijia-api": "^0.1.4", diff --git a/src/lib/powersync/connector.js b/src/lib/powersync/connector.js new file mode 100644 index 0000000..134f3db --- /dev/null +++ b/src/lib/powersync/connector.js @@ -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() + } +} diff --git a/src/lib/powersync/db.js b/src/lib/powersync/db.js new file mode 100644 index 0000000..e272f5e --- /dev/null +++ b/src/lib/powersync/db.js @@ -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 */ + } +} diff --git a/src/lib/powersync/schema.js b/src/lib/powersync/schema.js new file mode 100644 index 0000000..e38f9cc --- /dev/null +++ b/src/lib/powersync/schema.js @@ -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, +})