diff --git a/docs/WORK_PLAN_2026_Q1_P2.md b/docs/WORK_PLAN_2026_Q1_P2.md index 6ccc4683..5c1ddb3e 100644 --- a/docs/WORK_PLAN_2026_Q1_P2.md +++ b/docs/WORK_PLAN_2026_Q1_P2.md @@ -344,6 +344,8 @@ export class MultiTenancyPlugin implements RuntimePlugin { ## Q3 β€” Edge Runtime & Offline Sync > Status: **Planned** | Target: 2026-07 β€” 2026-09 +> πŸ“„ **Detailed Work Plan: [WORK_PLAN_2026_Q3.md](./WORK_PLAN_2026_Q3.md)** +> 🧩 **Type Contracts Defined:** `@objectql/types` β€” `sync.ts`, `edge.ts` ### Part A: Edge Runtime Support (4 weeks) diff --git a/docs/WORK_PLAN_2026_Q3.md b/docs/WORK_PLAN_2026_Q3.md new file mode 100644 index 00000000..c0ae985a --- /dev/null +++ b/docs/WORK_PLAN_2026_Q3.md @@ -0,0 +1,652 @@ +# ObjectQL Work Plan β€” 2026 Q3: Edge Runtime & Offline Sync + +> Created: 2026-02-08 | Status: **Planned** | Target: 2026-07 β€” 2026-09 +> Current Version: **4.2.0** | Prerequisite: Q1 Phase 2 (WASM Drivers), Q2 (Protocol Maturity) +> Parent Document: [WORK_PLAN_2026_Q1_P2.md](./WORK_PLAN_2026_Q1_P2.md) + +--- + +## Table of Contents + +- [Overview](#overview) +- [Part A: Edge Runtime Support](#part-a-edge-runtime-support) + - [Architecture](#architecture) + - [E-1: Cloudflare Workers Adapter](#e-1-cloudflare-workers-adapter) + - [E-2: Deno Deploy Validation](#e-2-deno-deploy-validation) + - [E-3: Vercel Edge Validation](#e-3-vercel-edge-validation) + - [E-4: Bun Compatibility](#e-4-bun-compatibility) + - [E-5: Edge Documentation](#e-5-edge-documentation) +- [Part B: Offline-First Sync Protocol](#part-b-offline-first-sync-protocol) + - [Sync Architecture](#sync-architecture) + - [SY-1: Sync Protocol Specification](#sy-1-sync-protocol-specification) + - [SY-2: Client-Side Change Tracking](#sy-2-client-side-change-tracking) + - [SY-3: Server Sync Endpoint](#sy-3-server-sync-endpoint) + - [SY-4: Conflict Resolution Engine](#sy-4-conflict-resolution-engine) + - [SY-5: Integration Tests](#sy-5-integration-tests) + - [SY-6: Documentation & Example PWA](#sy-6-documentation--example-pwa) +- [Type Contracts (Defined)](#type-contracts-defined) +- [Timeline](#timeline) +- [Success Criteria](#success-criteria) +- [Architecture Decisions](#architecture-decisions) + +--- + +## Overview + +ObjectQL Core is **universal** β€” zero Node.js native modules in `@objectql/core` or `@objectql/types`. Combined with browser WASM drivers (Q1) and protocol maturity (Q2), Q3 completes the platform story: + +1. **Edge Runtime Support** β€” Validate and adapt ObjectQL for Cloudflare Workers, Deno Deploy, Vercel Edge, and Bun. +2. **Offline-First Sync** β€” A bidirectional sync protocol between client-side WASM drivers and server-side data stores. + +**Prerequisites:** +- βœ… `@objectql/core` β€” Universal, no Node.js natives +- βœ… `@objectql/driver-memory` β€” Universal, runs in all environments +- πŸ”„ `@objectql/driver-sqlite-wasm` β€” Browser WASM driver (Q1 P2) +- πŸ”„ `@objectql/driver-pg-wasm` β€” Browser WASM driver (Q1 P2) +- πŸ”„ Protocol compliance β‰₯ 95% (Q2) + +--- + +## Part A: Edge Runtime Support + +> Duration: **4 weeks** | Priority: P0 + +### Architecture + +ObjectQL's edge strategy leverages the universal core and adapts driver bindings per platform: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Edge Request (HTTP) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Edge Adapter (per-platform) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ObjectQL Core (Universal) β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ QueryBuilder β†’ QueryAST β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ HookManager (Security, Validation) β”‚ β”‚ +β”‚ β”‚ └── Repository β†’ Driver β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Driver (platform-bound) β”‚ β”‚ +β”‚ β”‚ β€’ Cloudflare: D1 (SQLite) or Memory β”‚ β”‚ +β”‚ β”‚ β€’ Deno: Deno Postgres or Memory β”‚ β”‚ +β”‚ β”‚ β€’ Vercel: SDK (remote) or Memory β”‚ β”‚ +β”‚ β”‚ β€’ Bun: All Node.js drivers (native compat) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Key Principle: No new core changes.** Each edge adapter is a thin wrapper that: +1. Detects the runtime environment +2. Binds platform-native storage to an ObjectQL driver +3. Adapts the request/response lifecycle (stateless, request-scoped connections) + +### Edge Runtime Matrix + +| Runtime | Driver Options | Storage | Constraints | +|---------|---------------|---------|-------------| +| **Cloudflare Workers** | `driver-sqlite-wasm` (D1), `driver-memory` | D1, KV, R2 | 30s CPU, 128MB RAM | +| **Deno Deploy** | `driver-sql` (Deno Postgres), `driver-memory` | Deno KV, Deno Postgres | 50s wall-clock | +| **Vercel Edge** | `driver-sdk` (remote), `driver-memory` | External only (Vercel KV/Postgres via SDK) | 25s, 4MB body | +| **Bun** | All Node.js drivers | Full Node.js compat | No significant limits | + +### E-1: Cloudflare Workers Adapter + +| Field | Value | +|-------|-------| +| **Package** | `packages/adapters/cloudflare` | +| **NPM Name** | `@objectql/adapter-cloudflare` | +| **Priority** | P0 β€” Primary edge target | +| **Est.** | 2 weeks | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **E-1.1** | Package scaffolding (`package.json`, `tsconfig.json`, `wrangler.toml` template) | 2h | +| **E-1.2** | `CloudflareAdapter` class β€” request-scoped ObjectQL initialization | 4h | +| **E-1.3** | D1 driver binding β€” wrap Cloudflare D1 binding as SQLite-compatible Knex client | 8h | +| **E-1.4** | KV cache integration β€” optional query result caching via Cloudflare KV | 4h | +| **E-1.5** | Hono integration β€” `createObjectQLHandler(env)` factory for Hono on Workers | 4h | +| **E-1.6** | Environment detection utility (`isCloudflareWorker()`) | 1h | +| **E-1.7** | Unit tests (adapter initialization, D1 binding, request lifecycle) | 8h | +| **E-1.8** | Integration test with Miniflare (local Workers simulator) | 8h | +| **E-1.9** | Example Worker (`examples/edge/cloudflare-worker/`) | 4h | + +**Config Interface:** +```typescript +export interface CloudflareAdapterConfig { + /** D1 database binding name from wrangler.toml. Default: 'DB' */ + d1Binding?: string; + /** Optional KV namespace for query cache. Default: undefined (no cache) */ + kvCacheBinding?: string; + /** Cache TTL in seconds for KV-cached queries. Default: 60 */ + cacheTtl?: number; +} +``` + +**Key Decisions:** +1. D1 binding wraps the native `D1Database` API β€” no WASM needed (D1 is server-side SQLite). +2. The adapter creates a fresh ObjectQL instance per request (stateless). +3. Plugins (security, validator) are instantiated once, reused across requests. + +### E-2: Deno Deploy Validation + +| Field | Value | +|-------|-------| +| **Package** | `packages/adapters/deno` | +| **NPM Name** | `@objectql/adapter-deno` | +| **Priority** | P1 | +| **Est.** | 1 week | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **E-2.1** | Package scaffolding (Deno-compatible `deno.json` + npm compat) | 2h | +| **E-2.2** | `DenoAdapter` class β€” Deno.serve integration | 4h | +| **E-2.3** | Deno Postgres driver validation β€” verify `driver-sql` with `deno-postgres` client | 4h | +| **E-2.4** | Deno KV exploration β€” optional alternative to PostgreSQL for simple use cases | 4h | +| **E-2.5** | Unit tests (Deno test runner) | 4h | +| **E-2.6** | Example (`examples/edge/deno-deploy/`) | 4h | + +**Key Decisions:** +1. Deno has excellent Node.js compatibility β€” `@objectql/core` should work without modifications. +2. Primary driver: `@objectql/driver-sql` with Deno-native PostgreSQL client. +3. No Deno-specific driver needed; validation focuses on compatibility testing. + +### E-3: Vercel Edge Validation + +| Field | Value | +|-------|-------| +| **Package** | `packages/adapters/vercel-edge` | +| **NPM Name** | `@objectql/adapter-vercel-edge` | +| **Priority** | P1 | +| **Est.** | 3 days | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **E-3.1** | Package scaffolding | 1h | +| **E-3.2** | `VercelEdgeAdapter` β€” Next.js Edge Route handler factory | 4h | +| **E-3.3** | Validate `driver-sdk` and `driver-memory` in Edge Runtime | 4h | +| **E-3.4** | Example Next.js app (`examples/edge/vercel-edge/`) | 4h | +| **E-3.5** | Unit tests | 4h | + +**Key Decisions:** +1. Vercel Edge has no persistent storage β€” use `driver-sdk` to proxy to a remote ObjectQL server. +2. `driver-memory` is valid for read-heavy, cache-style workloads. +3. No custom driver needed. + +### E-4: Bun Compatibility + +| Field | Value | +|-------|-------| +| **Package** | No new package β€” compatibility validated in existing drivers | +| **Priority** | P2 | +| **Est.** | 3 days | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **E-4.1** | Run full driver TCK suite under Bun runtime | 4h | +| **E-4.2** | Fix any Bun-specific incompatibilities in core/drivers | 8h | +| **E-4.3** | Validate `bun:sqlite` as alternative to `better-sqlite3` | 4h | +| **E-4.4** | Example (`examples/edge/bun/`) | 2h | +| **E-4.5** | Document Bun-specific notes | 1h | + +**Key Decisions:** +1. Bun is a drop-in Node.js replacement β€” no adapter package needed. +2. `bun:sqlite` is a candidate for zero-dependency SQLite on Bun. +3. Focus: validate, don't rewrite. + +### E-5: Edge Documentation + +| Task | Description | Est. | +|------|-------------|------| +| **E-5.1** | `content/docs/server/edge.mdx` β€” Edge runtime overview and comparison | 4h | +| **E-5.2** | `content/docs/server/cloudflare.mdx` β€” Cloudflare Workers guide | 4h | +| **E-5.3** | `content/docs/server/deno.mdx` β€” Deno Deploy guide | 2h | +| **E-5.4** | `content/docs/server/vercel-edge.mdx` β€” Vercel Edge guide | 2h | +| **E-5.5** | `content/docs/server/bun.mdx` β€” Bun compatibility guide | 2h | +| **E-5.6** | Update `content/docs/server/meta.json` β€” Add edge runtime pages | 1h | + +--- + +## Part B: Offline-First Sync Protocol + +> Duration: **6 weeks** | Priority: P0 + +### Sync Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CLIENT (Browser) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ObjectQL Core β”‚ β”‚ Mutation Log β”‚ β”‚ +β”‚ β”‚ + WASM Driver │───▢│ (append-only, per-object) β”‚ β”‚ +β”‚ β”‚ (SQLite/PG) β”‚ β”‚ Stored in OPFS/IndexedDB β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sync Engine β”‚ β”‚ +β”‚ β”‚ β€’ Batch mutations β”‚ β”‚ +β”‚ β”‚ β€’ Push on reconnect β”‚ β”‚ +β”‚ β”‚ β€’ Apply server delta β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTP POST /api/sync + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SERVER β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sync Endpoint β”‚ β”‚ +β”‚ β”‚ 1. Receive client mutations β”‚ β”‚ +β”‚ β”‚ 2. Validate & apply (via ObjectQL Core + Hooks) β”‚ β”‚ +β”‚ β”‚ 3. Detect conflicts β”‚ β”‚ +β”‚ β”‚ 4. Return: mutation results + server changes since β”‚ β”‚ +β”‚ β”‚ client's last checkpoint β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Change Log (Server-side) β”‚ β”‚ +β”‚ β”‚ Append-only record of all mutations + server version β”‚ β”‚ +β”‚ β”‚ Used to compute delta for each client β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Protocol Flow:** + +1. **Online**: Client reads/writes directly via `driver-sdk` or WASM driver (no log). +2. **Offline**: Client writes to local WASM driver + appends to mutation log. +3. **Reconnect**: Sync engine pushes mutation log to server, receives server delta. +4. **Conflict**: Server detects conflicting versions, applies configured strategy. +5. **Resolution**: Client applies server delta, clears acknowledged mutations from log. + +### SY-1: Sync Protocol Specification + +| Field | Value | +|-------|-------| +| **Deliverable** | `@objectstack/spec` β€” Sync Protocol RFC | +| **Priority** | P0 β€” Must be defined before implementation | +| **Est.** | 1 week | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **SY-1.1** | Define wire format: `SyncPushRequest`, `SyncPushResponse` (JSON) | 4h | +| **SY-1.2** | Define `MutationLogEntry` schema (Zod) | 2h | +| **SY-1.3** | Define `SyncConflict` schema and resolution strategies | 4h | +| **SY-1.4** | Define checkpoint format (opaque server-assigned string) | 2h | +| **SY-1.5** | Define `SyncConfig` YAML schema (per-object opt-in) | 2h | +| **SY-1.6** | RFC document with protocol versioning strategy | 8h | + +**Protocol Versioning:** +``` +POST /api/sync HTTP/1.1 +Content-Type: application/json +X-ObjectQL-Sync-Version: 1 +``` + +### SY-2: Client-Side Change Tracking + +| Field | Value | +|-------|-------| +| **Package** | `packages/foundation/plugin-sync` | +| **NPM Name** | `@objectql/plugin-sync` | +| **Environment** | Universal (Browser + Node.js for testing) | +| **Priority** | P0 | +| **Est.** | 2 weeks | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **SY-2.1** | Package scaffolding (same structure as `plugin-validator`) | 1h | +| **SY-2.2** | `MutationLogger` β€” append-only mutation log backed by driver storage | 8h | +| **SY-2.3** | `SyncPlugin` implements `RuntimePlugin` β€” hooks into `afterCreate`, `afterUpdate`, `afterDelete` to record mutations | 4h | +| **SY-2.4** | `SyncEngine` β€” orchestrates push/pull cycle | 8h | +| **SY-2.5** | Online/offline detection (Navigator.onLine + heartbeat) | 4h | +| **SY-2.6** | Debounced batch sync (configurable via `SyncConfig.debounce_ms`) | 4h | +| **SY-2.7** | Client-side merge β€” apply server delta to local WASM driver | 8h | +| **SY-2.8** | Unit tests (`MutationLogger`, `SyncEngine`, merge logic) | 8h | +| **SY-2.9** | Mutation log compaction (remove acknowledged entries) | 4h | + +**Directory Structure:** +``` +packages/foundation/plugin-sync/ + package.json + tsconfig.json + vitest.config.ts + src/ + index.ts # Public exports + sync-plugin.ts # RuntimePlugin implementation + engine/ + sync-engine.ts # Push/pull orchestration + mutation-logger.ts # Append-only mutation log + merge-engine.ts # Apply server delta to local store + connectivity.ts # Online/offline detection + types.ts # Re-exports from @objectql/types/sync + __tests__/ + mutation-logger.spec.ts + sync-engine.spec.ts + merge-engine.spec.ts + sync-plugin.spec.ts +``` + +**Config Interface:** +```typescript +export interface SyncPluginConfig { + /** Server URL for sync endpoint. Required. */ + serverUrl: string; + + /** Authentication token provider */ + getAuthToken?: () => Promise; + + /** Sync interval in milliseconds when online. Default: 30000 (30s) */ + syncInterval?: number; + + /** Maximum retry attempts for failed sync. Default: 5 */ + maxRetries?: number; + + /** Callback when conflicts require manual resolution */ + onConflict?: (conflicts: SyncConflict[]) => Promise>>; +} +``` + +### SY-3: Server Sync Endpoint + +| Field | Value | +|-------|-------| +| **Package** | `packages/protocols/sync` | +| **NPM Name** | `@objectql/protocol-sync` | +| **Environment** | Node.js (server-side) | +| **Priority** | P0 | +| **Est.** | 2 weeks | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **SY-3.1** | Package scaffolding | 1h | +| **SY-3.2** | `SyncProtocolHandler` β€” HTTP POST handler for `/api/sync` | 8h | +| **SY-3.3** | Server-side change log β€” record all mutations with server version | 8h | +| **SY-3.4** | Delta computation β€” compute changes since client's checkpoint | 8h | +| **SY-3.5** | Mutation validation β€” apply client mutations through ObjectQL Core (hooks, security, validation) | 4h | +| **SY-3.6** | Optimistic concurrency β€” reject mutations with stale `baseVersion` | 4h | +| **SY-3.7** | Checkpoint management β€” generate opaque checkpoint tokens | 4h | +| **SY-3.8** | Rate limiting and request size validation | 4h | +| **SY-3.9** | Unit tests | 8h | +| **SY-3.10** | Integration with Hono server plugin | 4h | + +**Directory Structure:** +``` +packages/protocols/sync/ + package.json + tsconfig.json + vitest.config.ts + src/ + index.ts # Public exports + sync-handler.ts # HTTP handler + change-log.ts # Server-side change log + delta-computer.ts # Compute delta since checkpoint + version-manager.ts # Server version tracking + __tests__/ + sync-handler.spec.ts + change-log.spec.ts + delta-computer.spec.ts +``` + +**Server-Side Change Log Schema:** +```sql +-- Auto-created by ObjectQL when sync is enabled for any object +CREATE TABLE _objectql_change_log ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + object_name VARCHAR(255) NOT NULL, + record_id VARCHAR(255) NOT NULL, + operation VARCHAR(10) NOT NULL, -- 'create' | 'update' | 'delete' + data JSON, + version BIGINT NOT NULL, + client_id VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_object_version (object_name, version), + INDEX idx_created_at (created_at) +); +``` + +### SY-4: Conflict Resolution Engine + +| Field | Value | +|-------|-------| +| **Location** | `packages/foundation/plugin-sync/src/engine/conflict-resolver.ts` | +| **Priority** | P0 | +| **Est.** | 1 week | + +**Scope:** + +| Task | Description | Est. | +|------|-------------|------| +| **SY-4.1** | `LastWriteWinsResolver` β€” timestamp-based resolution | 4h | +| **SY-4.2** | `CrdtResolver` β€” field-level CRDT merge (LWW-Register per field) | 8h | +| **SY-4.3** | `ManualResolver` β€” flag conflicts for application-level resolution | 4h | +| **SY-4.4** | `ConflictResolverFactory` β€” select resolver based on `SyncConfig.strategy` | 2h | +| **SY-4.5** | Conflict audit trail β€” record conflict details for debugging | 4h | +| **SY-4.6** | Unit tests (all three strategies) | 8h | + +**Conflict Resolution Strategies:** + +| Strategy | Behavior | Use Case | +|----------|----------|----------| +| `last-write-wins` | Most recent timestamp wins. Per-record granularity. | Simple apps, low conflict probability | +| `crdt` | LWW-Register per field. Fields merge independently. No conflicts. | Collaborative editing, high concurrency | +| `manual` | Conflicts flagged to application via `onConflict` callback. | Business-critical data requiring human review | + +**CRDT Implementation (LWW-Register):** +```typescript +// Each field carries a (value, timestamp) pair. +// During merge, the field with the latest timestamp wins. +interface LWWField { + value: unknown; + timestamp: string; // ISO 8601 +} + +// Merge: for each conflicting field, compare timestamps +function mergeLWW(clientField: LWWField, serverField: LWWField): LWWField { + return clientField.timestamp > serverField.timestamp ? clientField : serverField; +} +``` + +### SY-5: Integration Tests + +| Task | Description | Est. | +|------|-------------|------| +| **SY-5.1** | End-to-end test: offline create β†’ reconnect β†’ sync β†’ verify server | 4h | +| **SY-5.2** | End-to-end test: concurrent edits β†’ conflict β†’ resolution | 4h | +| **SY-5.3** | End-to-end test: multi-client sync (3 clients, 1 server) | 8h | +| **SY-5.4** | Performance test: 1000 mutations batch sync | 4h | +| **SY-5.5** | Stress test: network interruption during sync (retry/resume) | 4h | + +### SY-6: Documentation & Example PWA + +| Task | Description | Est. | +|------|-------------|------| +| **SY-6.1** | `content/docs/data-access/offline-sync.mdx` β€” Sync protocol guide | 4h | +| **SY-6.2** | `content/docs/data-access/conflict-resolution.mdx` β€” Strategy comparison | 4h | +| **SY-6.3** | Example PWA (`examples/integrations/offline-pwa/`) β€” Todo app with offline sync | 8h | +| **SY-6.4** | Update `content/docs/data-access/meta.json` | 1h | +| **SY-6.5** | Update `content/docs/drivers/` β€” Note sync compatibility per driver | 2h | + +--- + +## Type Contracts (Defined) + +> Step 1 of the 4-Step Atomic Workflow: **Define the Type (Contract)** + +The following types have been added to `@objectql/types` as part of this work plan: + +### `packages/foundation/types/src/sync.ts` + +| Type | Purpose | +|------|---------| +| `SyncStrategy` | `'last-write-wins' \| 'crdt' \| 'manual'` | +| `SyncConfig` | Per-object sync configuration (YAML `sync:` key) | +| `MutationOperation` | `'create' \| 'update' \| 'delete'` | +| `MutationLogEntry` | Client-side mutation log record | +| `SyncConflict` | Server-detected merge conflict descriptor | +| `SyncMutationResult` | Per-mutation sync outcome (applied/conflict/rejected) | +| `SyncPushRequest` | Client β†’ Server sync payload | +| `SyncPushResponse` | Server β†’ Client sync response | +| `SyncServerChange` | Individual server-side change in delta | +| `SyncEndpointConfig` | Server sync endpoint configuration | + +### `packages/foundation/types/src/edge.ts` + +| Type | Purpose | +|------|---------| +| `EdgeRuntime` | Supported edge runtime discriminator | +| `EdgeDriverBinding` | Maps ObjectQL driver to edge-platform storage | +| `EdgeAdapterConfig` | Edge adapter configuration | +| `EdgeCapabilities` | Platform API availability declaration | +| `EDGE_CAPABILITIES` | Predefined capability profiles per runtime | + +### Updated Types + +| File | Change | +|------|--------| +| `object.ts` | Added `sync?: SyncConfig` to `ObjectConfig` | +| `driver.ts` | Added `mutationLog?: boolean` and `changeTracking?: boolean` to `DriverCapabilities` | +| `index.ts` | Added `export * from './sync'` and `export * from './edge'` | + +--- + +## Timeline + +| Week | Phase | Milestone | +|------|-------|-----------| +| **W1** | Edge | Cloudflare Workers adapter scaffolding + D1 binding | +| **W2** | Edge | Cloudflare Workers integration tests + Hono handler | +| **W3** | Edge | Deno Deploy + Vercel Edge validation | +| **W4** | Edge | Bun compatibility + Edge documentation | +| **W5** | Sync | Sync protocol spec (SY-1) + MutationLogger (SY-2 start) | +| **W6** | Sync | SyncPlugin + SyncEngine (SY-2 complete) | +| **W7** | Sync | Server sync endpoint (SY-3) | +| **W8** | Sync | Server change log + delta computation (SY-3 complete) | +| **W9** | Sync | Conflict resolution engine (SY-4) | +| **W10** | Sync | Integration tests (SY-5) + Documentation + Example PWA (SY-6) | + +--- + +## Success Criteria + +### Part A: Edge Runtime + +- [ ] Cloudflare Workers example deploys and passes CRUD operations via D1 +- [ ] Deno Deploy example serves ObjectQL queries via Deno Postgres +- [ ] Vercel Edge example proxies queries via `driver-sdk` +- [ ] Bun passes full driver TCK suite for `driver-sql` and `driver-memory` +- [ ] Edge documentation published (`content/docs/server/edge.mdx` + per-runtime guides) +- [ ] Zero changes to `@objectql/core` β€” all adaptation is in adapter packages + +### Part B: Offline-First Sync + +- [ ] Client-side mutation log records offline operations correctly +- [ ] Sync engine pushes mutations and receives server delta on reconnect +- [ ] Last-write-wins resolution works for concurrent field edits +- [ ] CRDT (LWW-Register) resolution merges fields without conflicts +- [ ] Manual resolution callback is invoked for flagged conflicts +- [ ] Server-side change log retains 30 days of history by default +- [ ] Example PWA works offline, syncs on reconnect, resolves conflicts +- [ ] Security: All sync mutations pass through ObjectQL hooks (RBAC, validation) +- [ ] Performance: 1000-mutation batch sync completes in < 5 seconds +- [ ] Sync protocol versioned (`X-ObjectQL-Sync-Version: 1`) for future evolution + +--- + +## Architecture Decisions + +### ADR-005: Edge adapters as separate packages + +**Context:** ObjectQL Core is already universal. Edge runtime support requires platform-specific binding code (D1, Deno KV, etc.) that should not pollute the core. + +**Decision:** Each edge runtime gets its own adapter package under `packages/adapters/`. These packages depend on `@objectql/core` and `@objectql/types` but introduce no changes to them. + +**Rationale:** +- Keeps core universal and dependency-free +- Adapter packages can version independently if needed +- Users only install the adapter for their target platform +- Platform-specific APIs (D1, Deno.serve) are isolated + +**Status:** Accepted. + +### ADR-006: Sync protocol as opt-in per object + +**Context:** Not all objects need offline sync. Global sync would create unnecessary overhead and complexity. + +**Decision:** Sync is configured per-object via the `sync` key in `*.object.yml`. Objects without `sync.enabled: true` are not tracked. + +**Rationale:** +- Minimizes performance impact (no mutation logging for non-synced objects) +- Gives developers explicit control over sync behavior +- Conflict resolution strategy can vary per object +- Aligns with ObjectQL's metadata-driven philosophy + +**Status:** Accepted. + +### ADR-007: Checkpoint-based sync (not timestamp-based) + +**Context:** Timestamp-based sync requires synchronized clocks between client and server. Clock skew causes data loss. + +**Decision:** Use server-assigned opaque checkpoint tokens. The server generates a checkpoint after each sync, the client stores it, and sends it back on the next sync. The server computes the delta since that checkpoint. + +**Rationale:** +- No clock synchronization required +- Server has full control over delta computation +- Checkpoints are tamper-resistant (server-generated) +- Compatible with both SQL (sequence numbers) and NoSQL (opaque tokens) backends + +**Status:** Accepted. + +### ADR-008: Sync mutations go through full ObjectQL hook pipeline + +**Context:** Offline mutations could bypass server-side security and validation if applied directly to the database. + +**Decision:** All client mutations received via the sync endpoint are replayed through the standard ObjectQL Repository β†’ Hook β†’ Driver pipeline. Security (`plugin-security`), validation (`plugin-validator`), and workflow (`plugin-workflow`) all apply. + +**Rationale:** +- No security bypass β€” RBAC/FLS/RLS enforced even for offline edits +- Validation rules catch invalid data before persistence +- State machine transitions are validated by workflow engine +- Audit trail is maintained for all mutations + +**Status:** Accepted. + +### ADR-009: CRDT strategy uses LWW-Register per field + +**Context:** Full CRDT implementations (e.g., Yjs, Automerge) are complex and require special data structures. ObjectQL operates on structured records, not collaborative text. + +**Decision:** Use LWW-Register (Last-Writer-Wins Register) at the field level. Each field independently resolves to the most recent write. This is a well-understood CRDT that works for structured data. + +**Rationale:** +- Simple to implement and reason about +- No special data structures needed β€” works with existing drivers +- Field-level granularity avoids whole-record conflicts +- Well-suited for form-based applications (most ObjectQL use cases) +- Can upgrade to more sophisticated CRDTs (e.g., counters, sets) in future versions + +**Status:** Accepted. + +--- + +## New Package Summary + +| Package | NPM Name | Location | Environment | +|---------|----------|----------|-------------| +| `@objectql/adapter-cloudflare` | `@objectql/adapter-cloudflare` | `packages/adapters/cloudflare` | Cloudflare Workers | +| `@objectql/adapter-deno` | `@objectql/adapter-deno` | `packages/adapters/deno` | Deno Deploy | +| `@objectql/adapter-vercel-edge` | `@objectql/adapter-vercel-edge` | `packages/adapters/vercel-edge` | Vercel Edge | +| `@objectql/plugin-sync` | `@objectql/plugin-sync` | `packages/foundation/plugin-sync` | Universal | +| `@objectql/protocol-sync` | `@objectql/protocol-sync` | `packages/protocols/sync` | Node.js | diff --git a/packages/foundation/types/src/driver.ts b/packages/foundation/types/src/driver.ts index ac6ebbc7..cbb43ea4 100644 --- a/packages/foundation/types/src/driver.ts +++ b/packages/foundation/types/src/driver.ts @@ -131,6 +131,12 @@ export interface DriverCapabilities { readonly connectionPooling?: boolean; readonly preparedStatements?: boolean; readonly queryCache?: boolean; + + // Sync support (Q3 β€” Offline-First Sync Protocol) + /** Driver can record mutations to an append-only log for offline sync */ + readonly mutationLog?: boolean; + /** Driver supports checkpoint-based change tracking */ + readonly changeTracking?: boolean; } /** diff --git a/packages/foundation/types/src/edge.ts b/packages/foundation/types/src/edge.ts new file mode 100644 index 00000000..1773eb16 --- /dev/null +++ b/packages/foundation/types/src/edge.ts @@ -0,0 +1,157 @@ +/** + * ObjectQL + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// ============================================================================ +// Edge Runtime Types +// ============================================================================ + +/** + * Supported edge runtime environments. + */ +export type EdgeRuntime = + | 'cloudflare-workers' + | 'deno-deploy' + | 'vercel-edge' + | 'bun' + | 'node'; + +/** + * Edge-specific driver binding. + * + * Maps an ObjectQL driver to an edge-platform-native storage primitive. + * For example, Cloudflare Workers binds `driver-sqlite-wasm` to D1. + */ +export interface EdgeDriverBinding { + /** + * The ObjectQL driver package name. + * e.g., `'@objectql/driver-memory'`, `'@objectql/driver-sqlite-wasm'` + */ + readonly driver: string; + + /** + * Edge-platform binding name (environment variable or resource identifier). + * e.g., `'D1_DATABASE'` for Cloudflare D1, `'POSTGRES_URL'` for Deno Postgres. + */ + readonly binding?: string; + + /** + * Driver-specific configuration overrides for the edge environment. + */ + readonly config?: Record; +} + +/** + * Edge runtime adapter configuration. + * + * Declares how ObjectQL should adapt to a specific edge runtime. + * Used by the edge adapter packages (e.g., `@objectql/adapter-cloudflare`). + */ +export interface EdgeAdapterConfig { + /** Target edge runtime */ + readonly runtime: EdgeRuntime; + + /** + * Driver bindings for this edge environment. + * Maps datasource names to edge-specific driver bindings. + */ + readonly bindings?: Record; + + /** + * Maximum execution time in milliseconds. + * Edge runtimes impose strict CPU time limits. + * Default varies by runtime (e.g., 30000 for Cloudflare Workers). + */ + readonly maxExecutionTime?: number; + + /** + * Enable request-scoped driver connections. + * Edge runtimes are stateless; connections are created per-request. + * Default: true + */ + readonly requestScoped?: boolean; +} + +/** + * Edge runtime environment capabilities. + * + * Declares what platform APIs are available in the target edge runtime. + * Used by ObjectQL to select compatible code paths. + */ +export interface EdgeCapabilities { + /** WebAssembly support */ + readonly wasm: boolean; + + /** Persistent storage available (OPFS, KV, D1, etc.) */ + readonly persistentStorage: boolean; + + /** WebSocket support for real-time sync */ + readonly webSocket: boolean; + + /** Cron/scheduled trigger support */ + readonly scheduledTriggers: boolean; + + /** Maximum request body size in bytes */ + readonly maxRequestBodySize?: number; + + /** Maximum execution time in milliseconds */ + readonly maxExecutionTime?: number; + + /** Available storage primitives */ + readonly storagePrimitives: readonly string[]; +} + +/** + * Predefined edge capability profiles for known runtimes. + */ +export const EDGE_CAPABILITIES: Readonly> = { + 'cloudflare-workers': { + wasm: true, + persistentStorage: true, + webSocket: true, + scheduledTriggers: true, + maxRequestBodySize: 100 * 1024 * 1024, // 100MB + maxExecutionTime: 30_000, + storagePrimitives: ['D1', 'KV', 'R2', 'Durable Objects'], + }, + 'deno-deploy': { + wasm: true, + persistentStorage: true, + webSocket: true, + scheduledTriggers: true, + maxRequestBodySize: 512 * 1024, // 512KB + maxExecutionTime: 50_000, + storagePrimitives: ['Deno KV', 'Deno Postgres'], + }, + 'vercel-edge': { + wasm: true, + persistentStorage: false, + webSocket: false, + scheduledTriggers: false, + maxRequestBodySize: 4 * 1024 * 1024, // 4MB + maxExecutionTime: 25_000, + storagePrimitives: ['KV (via Vercel KV)', 'Postgres (via Vercel Postgres)'], + }, + 'bun': { + wasm: true, + persistentStorage: true, + webSocket: true, + scheduledTriggers: false, + maxRequestBodySize: Infinity, + maxExecutionTime: Infinity, + storagePrimitives: ['SQLite (bun:sqlite)', 'File System'], + }, + 'node': { + wasm: true, + persistentStorage: true, + webSocket: true, + scheduledTriggers: false, + maxRequestBodySize: Infinity, + maxExecutionTime: Infinity, + storagePrimitives: ['All drivers'], + }, +}; diff --git a/packages/foundation/types/src/index.ts b/packages/foundation/types/src/index.ts index c237e354..ea501ea4 100644 --- a/packages/foundation/types/src/index.ts +++ b/packages/foundation/types/src/index.ts @@ -36,3 +36,5 @@ export * from './plugin'; export * from './gateway'; export * from './logger'; export * from './ai'; +export * from './sync'; +export * from './edge'; diff --git a/packages/foundation/types/src/object.ts b/packages/foundation/types/src/object.ts index 19808d9c..5bb65cf0 100644 --- a/packages/foundation/types/src/object.ts +++ b/packages/foundation/types/src/object.ts @@ -11,6 +11,7 @@ import { z } from 'zod'; import { FieldConfig } from './field'; import { ActionConfig } from './action'; import { AnyValidationRule } from './validation'; +import { SyncConfig } from './sync'; /** * Re-export Protocol Types from @objectstack/spec 1.1.0 @@ -170,6 +171,20 @@ export interface ObjectConfig { validation_strategy?: string; }; }; + + /** + * Offline-First Sync configuration (RUNTIME ONLY). + * Opt-in per object. See {@link SyncConfig} for details. + * + * @example + * ```yaml + * sync: + * enabled: true + * strategy: last-write-wins + * conflict_fields: [status] + * ``` + */ + sync?: SyncConfig; } /** diff --git a/packages/foundation/types/src/sync.ts b/packages/foundation/types/src/sync.ts new file mode 100644 index 00000000..3d691af0 --- /dev/null +++ b/packages/foundation/types/src/sync.ts @@ -0,0 +1,223 @@ +/** + * ObjectQL + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// ============================================================================ +// Offline-First Sync Protocol Types +// ============================================================================ + +/** + * Sync conflict resolution strategy. + * + * - `last-write-wins`: Server accepts the most recent mutation based on timestamp. + * - `crdt`: Conflict-free Replicated Data Type β€” field-level merge without conflicts. + * - `manual`: Conflicts are flagged for manual resolution via a callback. + */ +export type SyncStrategy = 'last-write-wins' | 'crdt' | 'manual'; + +/** + * Per-object sync configuration. + * + * Declared in `*.object.yml` under the `sync` key. Opt-in per object. + * + * @example + * ```yaml + * name: story + * sync: + * enabled: true + * strategy: last-write-wins + * conflict_fields: [status] + * ``` + */ +export interface SyncConfig { + /** Enable offline sync for this object. Default: false */ + readonly enabled: boolean; + + /** Conflict resolution strategy. Default: 'last-write-wins' */ + readonly strategy?: SyncStrategy; + + /** + * Fields requiring manual merge when strategy is 'last-write-wins'. + * Changes to these fields during concurrent edits trigger a conflict. + */ + readonly conflict_fields?: readonly string[]; + + /** + * Sync direction. + * - `bidirectional`: Client ↔ Server (default) + * - `push-only`: Client β†’ Server + * - `pull-only`: Server β†’ Client + */ + readonly direction?: 'bidirectional' | 'push-only' | 'pull-only'; + + /** + * Debounce interval in milliseconds before syncing mutations. + * Batches rapid mutations into a single sync request. + * Default: 1000 + */ + readonly debounce_ms?: number; + + /** + * Maximum number of mutations to batch in a single sync request. + * Default: 50 + */ + readonly batch_size?: number; +} + +/** + * Mutation operation type recorded in the client-side mutation log. + */ +export type MutationOperation = 'create' | 'update' | 'delete'; + +/** + * A single entry in the client-side append-only mutation log. + * + * Recorded by WASM drivers when offline. Replayed to the server + * during sync to achieve eventual consistency. + */ +export interface MutationLogEntry { + /** Unique mutation identifier (UUID v7 for time-ordered sorting) */ + readonly id: string; + + /** Object name this mutation applies to */ + readonly objectName: string; + + /** Record identifier */ + readonly recordId: string | number; + + /** Type of mutation */ + readonly operation: MutationOperation; + + /** + * The mutation payload. + * - `create`: Full record data. + * - `update`: Partial field updates (only changed fields). + * - `delete`: undefined. + */ + readonly data?: Record; + + /** ISO 8601 timestamp when the mutation was recorded on the client */ + readonly timestamp: string; + + /** Client device identifier for multi-device conflict resolution */ + readonly clientId: string; + + /** Monotonically increasing sequence number per client */ + readonly sequence: number; + + /** + * Server-assigned version of the record at the time of mutation. + * Used for optimistic concurrency during sync. + * `null` for new records created offline. + */ + readonly baseVersion: number | null; +} + +/** + * Conflict descriptor returned when the server detects a merge conflict. + */ +export interface SyncConflict { + /** Object name */ + readonly objectName: string; + + /** Record identifier */ + readonly recordId: string | number; + + /** The client's mutation that caused the conflict */ + readonly clientMutation: MutationLogEntry; + + /** Current server-side record state */ + readonly serverRecord: Record; + + /** Fields that are in conflict */ + readonly conflictingFields: readonly string[]; + + /** Suggested resolution (server-computed) */ + readonly suggestedResolution?: Record; +} + +/** + * Outcome of a single mutation during sync. + */ +export type SyncMutationResult = + | { readonly status: 'applied'; readonly serverVersion: number } + | { readonly status: 'conflict'; readonly conflict: SyncConflict } + | { readonly status: 'rejected'; readonly reason: string }; + +/** + * Client β†’ Server sync request payload. + */ +export interface SyncPushRequest { + /** Client device identifier */ + readonly clientId: string; + + /** Mutations to push, ordered by sequence number */ + readonly mutations: readonly MutationLogEntry[]; + + /** + * Last server checkpoint the client has seen. + * The server uses this to determine what changes to send back. + */ + readonly lastCheckpoint: string | null; +} + +/** + * Server β†’ Client sync response payload. + */ +export interface SyncPushResponse { + /** Results for each pushed mutation (same order as request) */ + readonly results: readonly SyncMutationResult[]; + + /** Server changes since the client's lastCheckpoint */ + readonly serverChanges: readonly SyncServerChange[]; + + /** New checkpoint for the client to store */ + readonly checkpoint: string; +} + +/** + * A server-side change to be applied on the client. + */ +export interface SyncServerChange { + /** Object name */ + readonly objectName: string; + + /** Record identifier */ + readonly recordId: string | number; + + /** Operation that occurred on the server */ + readonly operation: MutationOperation; + + /** Record data (full for create, partial for update, undefined for delete) */ + readonly data?: Record; + + /** Server version after this change */ + readonly serverVersion: number; + + /** ISO 8601 timestamp of the server change */ + readonly timestamp: string; +} + +/** + * Sync endpoint configuration for the server-side sync service. + */ +export interface SyncEndpointConfig { + /** Enable the sync endpoint. Default: false */ + readonly enabled: boolean; + + /** URL path for the sync endpoint. Default: '/api/sync' */ + readonly path?: string; + + /** Maximum mutations per push request. Default: 100 */ + readonly maxMutationsPerRequest?: number; + + /** Maximum age (in days) for server-side change log retention. Default: 30 */ + readonly changeLogRetentionDays?: number; + + /** Enable WebSocket for real-time push from server. Default: false */ + readonly realtime?: boolean; +}