@terreno/mcp 0.31.0 → 56.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +8 -9
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
- package/dist/docs/upgrades/56.0.0-beta.2.md +59 -0
- package/dist/docs/versioned/next/README.md +2 -0
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
- package/dist/docs/versioned/next/how-to/README.md +2 -0
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
- package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
- package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
- package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
- package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
- package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
- package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
- package/dist/docs/versioned/next/reference/README.md +2 -0
- package/dist/docs/versioned/next/reference/comms.md +132 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +9 -0
- package/dist/docs/versioned/next/reference/syncdb.md +456 -0
- package/dist/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
- package/dist/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
- package/dist/docs/versioned/next/tasks/comms-abstraction.md +29 -16
- package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
- package/dist/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
- package/dist/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
- package/dist/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
- package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
- package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
- package/dist/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
- package/dist/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
- package/package.json +3 -3
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
- package/src/docs/upgrades/56.0.0-beta.2.md +59 -0
- package/src/docs/versioned/next/README.md +2 -0
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
- package/src/docs/versioned/next/how-to/README.md +2 -0
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
- package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
- package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
- package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
- package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
- package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
- package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
- package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
- package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
- package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
- package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
- package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
- package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
- package/src/docs/versioned/next/reference/README.md +2 -0
- package/src/docs/versioned/next/reference/comms.md +132 -0
- package/src/docs/versioned/next/reference/environment-variables.md +9 -0
- package/src/docs/versioned/next/reference/syncdb.md +456 -0
- package/src/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
- package/src/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
- package/src/docs/versioned/next/tasks/comms-abstraction.md +29 -16
- package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
- package/src/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
- package/src/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
- package/src/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
- package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
- package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
- package/src/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
- package/src/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
- package/dist/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/__tests__/bootstrap.test.d.ts.map +0 -1
- package/dist/__tests__/bootstrap.test.js +0 -299
- package/dist/__tests__/bootstrap.test.js.map +0 -1
- package/dist/__tests__/databaseQuery.test.d.ts +0 -2
- package/dist/__tests__/databaseQuery.test.d.ts.map +0 -1
- package/dist/__tests__/databaseQuery.test.js +0 -83
- package/dist/__tests__/databaseQuery.test.js.map +0 -1
- package/dist/__tests__/docsRoot.test.d.ts +0 -2
- package/dist/__tests__/docsRoot.test.d.ts.map +0 -1
- package/dist/__tests__/docsRoot.test.js +0 -59
- package/dist/__tests__/docsRoot.test.js.map +0 -1
- package/dist/__tests__/inferPackages.test.d.ts +0 -2
- package/dist/__tests__/inferPackages.test.d.ts.map +0 -1
- package/dist/__tests__/inferPackages.test.js +0 -35
- package/dist/__tests__/inferPackages.test.js.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/dist/__tests__/preload.js +0 -12
- package/dist/__tests__/preload.js.map +0 -1
- package/dist/__tests__/prompts.test.d.ts +0 -2
- package/dist/__tests__/prompts.test.d.ts.map +0 -1
- package/dist/__tests__/prompts.test.js +0 -295
- package/dist/__tests__/prompts.test.js.map +0 -1
- package/dist/__tests__/resources.test.d.ts +0 -2
- package/dist/__tests__/resources.test.d.ts.map +0 -1
- package/dist/__tests__/resources.test.js +0 -352
- package/dist/__tests__/resources.test.js.map +0 -1
- package/dist/__tests__/search.test.d.ts +0 -2
- package/dist/__tests__/search.test.d.ts.map +0 -1
- package/dist/__tests__/search.test.js +0 -163
- package/dist/__tests__/search.test.js.map +0 -1
- package/dist/__tests__/tools.test.d.ts +0 -2
- package/dist/__tests__/tools.test.d.ts.map +0 -1
- package/dist/__tests__/tools.test.js +0 -540
- package/dist/__tests__/tools.test.js.map +0 -1
- package/dist/__tests__/upgradeGuide.test.d.ts +0 -2
- package/dist/__tests__/upgradeGuide.test.d.ts.map +0 -1
- package/dist/__tests__/upgradeGuide.test.js +0 -22
- package/dist/__tests__/upgradeGuide.test.js.map +0 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# @terreno/syncdb API inventory (temporary)
|
|
2
|
+
|
|
3
|
+
Working document for the RTK → syncdb migration. Lists every public export from the three package entry points, with source locations verified against `syncdb/src/`.
|
|
4
|
+
|
|
5
|
+
## M5 — Does `bun run sdk` still work?
|
|
6
|
+
|
|
7
|
+
**Yes.** The example app still generates RTK Query hooks from the backend OpenAPI spec:
|
|
8
|
+
|
|
9
|
+
- `example-frontend/package.json:73` — `"sdk": "bun scripts/generate-sdk.ts && bun biome check --write scripts/generate-sdk.ts"`
|
|
10
|
+
- `example-frontend/scripts/generate-sdk.ts:22` — runs `@rtk-query/codegen-openapi` against `openapi-config.ts`
|
|
11
|
+
- `example-frontend/openapi-config.ts:11` — output is `./store/openApiSdk.ts`
|
|
12
|
+
|
|
13
|
+
**What it generates now (post-migration):** typed RTK Query hooks for **non-synced** routes — Better Auth, `GET /auth/me`, admin, AI explorer, version config, feature-flag configuration, and any custom REST endpoints. Synced collections (e.g. todos) are **not** read or written through generated hooks; they use `@terreno/syncdb` hooks instead. `example-frontend/store/sdk.ts` still wraps `openApiSdk.ts` with tag enhancements and realtime wiring for endpoints that remain on RTK (todos realtime config is legacy/unused by the syncdb screen).
|
|
14
|
+
|
|
15
|
+
## M7 — Local-first only vs opt-in?
|
|
16
|
+
|
|
17
|
+
**Local-first only — there is no server-first mode and no opt-in flag.**
|
|
18
|
+
|
|
19
|
+
The on-device TinyBase store is always the UI source of truth; reads come from local state, writes apply optimistically, and the server reconciles asynchronously.
|
|
20
|
+
|
|
21
|
+
- `syncdb/src/index.ts:4-7` — package doc: "reads come from the local store, writes apply optimistically… the server is asynchronous reconciliation"
|
|
22
|
+
- `syncdb/README.md:3-4` — "TinyBase MergeableStore … is the UI's source of truth"
|
|
23
|
+
- `syncdb/src/client.ts:60-151` (`SyncDbConfig`) — no `mode`, `serverFirst`, or similar option
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## `@terreno/syncdb` (`syncdb/src/index.ts`)
|
|
28
|
+
|
|
29
|
+
### Client factory and constants
|
|
30
|
+
|
|
31
|
+
| Symbol | Kind | Description | Source |
|
|
32
|
+
|--------|------|-------------|--------|
|
|
33
|
+
| `createSyncDb` | `function (config: SyncDbConfig): SyncDb` | Assemble the local-first client | `client.ts:281` |
|
|
34
|
+
| `DEFAULT_RECONCILE_INTERVAL_MS` | `const` (5 min) | Default periodic reconcile interval | `client.ts:38` |
|
|
35
|
+
| `DEFAULT_SEQ_JUMP_RECONCILE_MIN_INTERVAL_MS` | `const` (30s) | Seq-jump reconcile rate limit | `client.ts:41` |
|
|
36
|
+
| `DEFAULT_TOMBSTONE_RETENTION_MS` | `const` (90 days) | Client tombstone compaction window | `client.ts:44` |
|
|
37
|
+
| `DEFAULT_START_AUTH_RETRY_ATTEMPTS` | `const` (3) | `start()` auth-resolution retries | `client.ts:55` |
|
|
38
|
+
| `DEFAULT_START_AUTH_RETRY_DELAY_MS` | `const` (250) | Delay between `start()` auth retries | `client.ts:56` |
|
|
39
|
+
|
|
40
|
+
### `SyncDbConfig` fields
|
|
41
|
+
|
|
42
|
+
| Field | Type | Default | Description | Source |
|
|
43
|
+
|-------|------|---------|-------------|--------|
|
|
44
|
+
| `name` | `string` | — (required) | Persisted database name | `client.ts:62` |
|
|
45
|
+
| `collections` | `string[]` | — (required) | Collections to sync | `client.ts:64` |
|
|
46
|
+
| `authProvider` | `AuthProvider` | — (required) | Token/user/auth-change surface | `client.ts:65` |
|
|
47
|
+
| `baseUrl` | `string?` | — | Server origin; required unless `transport` + `httpChannel` injected | `client.ts:67` |
|
|
48
|
+
| `transport` | `SyncTransport?` | socket transport from `baseUrl` | Transport override (tests) | `client.ts:69` |
|
|
49
|
+
| `httpChannel` | `HttpChannel?` | from `baseUrl` | HTTP channel override | `client.ts:71` |
|
|
50
|
+
| `persisterFactory` | `PersisterFactory?` | platform default | Persister override | `client.ts:73` |
|
|
51
|
+
| `keyProvider` | `KeyProvider?` | server-derived via `/sync/key` | Web encryption key provider | `client.ts:75` |
|
|
52
|
+
| `idbGetImpl` | test hook | — | IndexedDB read override (tests) | `client.ts:82` |
|
|
53
|
+
| `idbSetImpl` | test hook | — | IndexedDB write override (tests) | `client.ts:83` |
|
|
54
|
+
| `reconcileIntervalMs` | `number?` | `DEFAULT_RECONCILE_INTERVAL_MS` (5 min); `0` disables | Periodic reconcile timer | `client.ts:85` |
|
|
55
|
+
| `seqJumpReconcileMinIntervalMs` | `number?` | `DEFAULT_SEQ_JUMP_RECONCILE_MIN_INTERVAL_MS` | Per-stream seq-jump rate limit | `client.ts:87` |
|
|
56
|
+
| `now` | `() => number?` | `Date.now` | Injectable clock | `client.ts:89` |
|
|
57
|
+
| `random` | `() => number?` | `Math.random` | Injectable RNG for backoff jitter | `client.ts:91` |
|
|
58
|
+
| `debug` | `boolean \| SyncDebugLogOptions?` | `false` (off) | In-memory debug event log | `client.ts:98` |
|
|
59
|
+
| `onAuthRequired` | `() => void?` | — | Fires once per auth-pause episode | `client.ts:104` |
|
|
60
|
+
| `wipeOnSignOut` | `boolean?` | `false` | `signOut()` also wipes local data | `client.ts:111` |
|
|
61
|
+
| `batchSize` | `number?` | `50` (server caps at 100) | Max mutations per batched drain | `client.ts:113` |
|
|
62
|
+
| `haltQueueOnConflict` | `boolean?` | `false` | Conflict halts entire drain vs per-entity | `client.ts:119` |
|
|
63
|
+
| `onDecryptFailure` | `() => void?` | wipe + re-bootstrap (with `console.warn`) | Override decrypt-failure recovery | `client.ts:129` |
|
|
64
|
+
| `tombstoneRetentionMs` | `number?` | `DEFAULT_TOMBSTONE_RETENTION_MS`; `0` disables | Client tombstone compaction after reconcile | `client.ts:138` |
|
|
65
|
+
| `startAuthRetryAttempts` | `number?` | `DEFAULT_START_AUTH_RETRY_ATTEMPTS`; `1` disables | `start()` user-id resolution attempts | `client.ts:148` |
|
|
66
|
+
| `startAuthRetryDelayMs` | `number?` | `DEFAULT_START_AUTH_RETRY_DELAY_MS` | Delay between those attempts | `client.ts:150` |
|
|
67
|
+
|
|
68
|
+
### `SyncDb` methods and properties
|
|
69
|
+
|
|
70
|
+
| Member | Signature / type | Description | Source |
|
|
71
|
+
|--------|------------------|-------------|--------|
|
|
72
|
+
| `store` | `readonly SyncStore` | Local-first entity store | `client.ts:193` |
|
|
73
|
+
| `outbox` | `readonly Outbox` | Durable mutation outbox | `client.ts:195` |
|
|
74
|
+
| `debug` | `readonly SyncDebugLog \| undefined` | Debug log when `debug` enabled | `client.ts:270` |
|
|
75
|
+
| `start` | `() => Promise<void>` | Resolve user, persist, connect, bootstrap | `client.ts:202` |
|
|
76
|
+
| `stop` | `() => Promise<void>` | Disconnect, stop persistence, clear timers | `client.ts:204` |
|
|
77
|
+
| `goOffline` | `() => void` | Simulated outage (local writes continue) | `client.ts:212` |
|
|
78
|
+
| `goOnline` | `() => Promise<void>` | End simulated outage | `client.ts:218` |
|
|
79
|
+
| `mutate` | `(args: MutateArgs) => {mutationId, id}` | Optimistic write + enqueue + replay | `client.ts:224` |
|
|
80
|
+
| `reconcile` | `() => Promise<void>` | HTTP snapshot catch-up all streams | `client.ts:226` |
|
|
81
|
+
| `forceResync` | `() => Promise<ForceResyncResult>` | Purge streams and re-bootstrap from 0 | `client.ts:233` |
|
|
82
|
+
| `replayOutbox` | `() => Promise<void>` | Drain queued mutations now | `client.ts:235` |
|
|
83
|
+
| `resolveConflict` | `({mutationId, strategy}) => void` | Apply `useServer` or `keepMine` | `client.ts:237` |
|
|
84
|
+
| `retryFailed` | `({entityId}) => void` | Re-enable blocked entity queue | `client.ts:244` |
|
|
85
|
+
| `signOut` | `() => Promise<void>` | Explicit teardown; optional wipe | `client.ts:254` |
|
|
86
|
+
| `getSyncStatus` | `() => SyncStatus` | Aggregate sync state snapshot | `client.ts:256` |
|
|
87
|
+
| `onStatusChange` | `(callback) => () => void` | Subscribe to connectivity/syncing changes | `client.ts:264` |
|
|
88
|
+
|
|
89
|
+
### Supporting client types
|
|
90
|
+
|
|
91
|
+
| Symbol | Kind | Source |
|
|
92
|
+
|--------|------|--------|
|
|
93
|
+
| `MutateArgs` | `interface` `{collection, operation, id?, data?}` | `client.ts:182` |
|
|
94
|
+
| `ForceResyncSkipReason` | `type` union | `client.ts:162` |
|
|
95
|
+
| `ForceResyncResult` | `interface` | `client.ts:170` |
|
|
96
|
+
|
|
97
|
+
### Protocol, status, and conflict types (`types.ts`)
|
|
98
|
+
|
|
99
|
+
| Symbol | Kind | Source |
|
|
100
|
+
|--------|------|--------|
|
|
101
|
+
| `SyncMutationOperation` | `type` `"create" \| "update" \| "delete"` | `types.ts:9` |
|
|
102
|
+
| `SyncDelta` | `interface` | `types.ts:12` |
|
|
103
|
+
| `SyncMutateRequest` | `interface` | `types.ts:35` |
|
|
104
|
+
| `SyncAck` | `interface` | `types.ts:49` |
|
|
105
|
+
| `SyncNackCode` | `type` | `types.ts:63` |
|
|
106
|
+
| `SyncNack` | `interface` | `types.ts:66` |
|
|
107
|
+
| `SyncMutateBatchRequest` | `interface` | `types.ts:94` |
|
|
108
|
+
| `SyncMutateBatchResult` | `type` | `types.ts:107` |
|
|
109
|
+
| `SyncMutateBatchResponse` | `interface` | `types.ts:116` |
|
|
110
|
+
| `SyncSnapshotEntity` | `interface` | `types.ts:121` |
|
|
111
|
+
| `SyncEntitiesResponse` | `interface` | `types.ts:129` |
|
|
112
|
+
| `SyncSnapshotResponse` | `interface` | `types.ts:134` |
|
|
113
|
+
| `SyncStreamInfo` | `interface` | `types.ts:156` |
|
|
114
|
+
| `OutboxStatus` | `type` | `types.ts:164` |
|
|
115
|
+
| `OutboxMutation` | `interface` | `types.ts:167` |
|
|
116
|
+
| `SyncConflict` | `interface` | `types.ts:186` |
|
|
117
|
+
| `ConflictResolutionStrategy` | `type` `"useServer" \| "keepMine"` | `types.ts:206` |
|
|
118
|
+
| `SyncCollectionStatus` | `interface` | `types.ts:213` |
|
|
119
|
+
| `SyncStatus` | `interface` | `types.ts:220` |
|
|
120
|
+
| `AuthProvider` | `interface` | `types.ts:269` |
|
|
121
|
+
|
|
122
|
+
### Store / outbox / wipe
|
|
123
|
+
|
|
124
|
+
| Symbol | Kind | Source |
|
|
125
|
+
|--------|------|--------|
|
|
126
|
+
| `listConflicts` | `function` | `mutations/conflicts.ts:71` |
|
|
127
|
+
| `generateMutationId` | `function` | `mutations/outbox.ts` (re-export `index.ts:75`) |
|
|
128
|
+
| `Outbox` | `type` | `mutations/outbox.ts` |
|
|
129
|
+
| `SyncStore` | `type` | `storage/store.ts` |
|
|
130
|
+
| `OUTBOX_TABLE` | `const` | `storage/types.ts` |
|
|
131
|
+
| `SyncEntity` | `type` | `storage/types.ts` |
|
|
132
|
+
| `wipeLocalData` | `async function` | `storage/wipe.ts:19` |
|
|
133
|
+
|
|
134
|
+
### Auth adapter
|
|
135
|
+
|
|
136
|
+
| Symbol | Kind | Source |
|
|
137
|
+
|--------|------|--------|
|
|
138
|
+
| `betterAuthAdapter` | `function` | `auth/betterAuthAdapter.ts:39` |
|
|
139
|
+
| `DEFAULT_AUTH_POLL_INTERVAL_MS` | `const` (5000) | `auth/betterAuthAdapter.ts:5` |
|
|
140
|
+
| `BetterAuthAdapterOptions` | `interface` | `auth/betterAuthAdapter.ts:7` |
|
|
141
|
+
| `BetterAuthClientLike` | `type` | `auth/types.ts` |
|
|
142
|
+
| `BetterAuthGetSessionResult` | `type` | `auth/types.ts` |
|
|
143
|
+
| `BetterAuthSessionAtomLike` | `type` | `auth/types.ts` |
|
|
144
|
+
| `BetterAuthSessionDataLike` | `type` | `auth/types.ts` |
|
|
145
|
+
| `BetterAuthSessionLike` | `type` | `auth/types.ts` |
|
|
146
|
+
| `BetterAuthUserLike` | `type` | `auth/types.ts` |
|
|
147
|
+
|
|
148
|
+
### Encryption and key providers
|
|
149
|
+
|
|
150
|
+
| Symbol | Kind | Source |
|
|
151
|
+
|--------|------|--------|
|
|
152
|
+
| `AES_GCM_ENVELOPE_VERSION` | `const` | `crypto/aesGcmCodec.ts` |
|
|
153
|
+
| `createAesGcmCodec` | `function` | `crypto/aesGcmCodec.ts` |
|
|
154
|
+
| `PayloadIntegrityError` | `class` | `crypto/aesGcmCodec.ts` |
|
|
155
|
+
| `UnknownEnvelopeVersionError` | `class` | `crypto/aesGcmCodec.ts` |
|
|
156
|
+
| `identityCodec` | `const` | `crypto/identityCodec.ts` |
|
|
157
|
+
| `createKeyProviderCodec` | `function` | `crypto/keyProviders.ts` |
|
|
158
|
+
| `createLocalKeyProvider` | `function` | `crypto/keyProviders.ts` |
|
|
159
|
+
| `createServerKeyProvider` | `function` | `crypto/keyProviders.ts` |
|
|
160
|
+
| `DEFAULT_KEY_CACHE_DB_NAME` | `const` | `crypto/keyProviders.ts` |
|
|
161
|
+
| `KeyProvider` | `interface` | `crypto/types.ts` |
|
|
162
|
+
| `PayloadCodec` | `interface` | `crypto/types.ts` |
|
|
163
|
+
|
|
164
|
+
### Persisters
|
|
165
|
+
|
|
166
|
+
| Symbol | Kind | Source |
|
|
167
|
+
|--------|------|--------|
|
|
168
|
+
| `createDefaultPersisterFactory` | `function` | `persisters/defaultPersisterFactory.ts` |
|
|
169
|
+
| `createEncryptedIndexedDbPersister` | `function` | `persisters/encryptedIndexedDbPersister.ts` |
|
|
170
|
+
| `clearMemoryPersisterData` | `function` | `persisters/memoryPersister.ts` |
|
|
171
|
+
| `createMemoryPersister` | `function` | `persisters/memoryPersister.ts` |
|
|
172
|
+
| `memoryPersisterFactory` | `function` | `persisters/memoryPersister.ts` |
|
|
173
|
+
| `DefaultPersisterFactoryConfig` | `type` | `persisters/types.ts` |
|
|
174
|
+
| `PersisterFactory` | `type` | `persisters/types.ts` |
|
|
175
|
+
|
|
176
|
+
### Transports and HTTP channel
|
|
177
|
+
|
|
178
|
+
| Symbol | Kind | Source |
|
|
179
|
+
|--------|------|--------|
|
|
180
|
+
| `AuthRequiredError` | `class` | `sync/httpChannel.ts` |
|
|
181
|
+
| `createHttpChannel` | `function` | `sync/httpChannel.ts` |
|
|
182
|
+
| `FetchLike` | `type` | `sync/httpChannel.ts` |
|
|
183
|
+
| `FetchSnapshotPageArgs` | `type` | `sync/httpChannel.ts` |
|
|
184
|
+
| `HttpChannel` | `interface` | `sync/httpChannel.ts` |
|
|
185
|
+
| `HttpChannelConfig` | `interface` | `sync/httpChannel.ts` |
|
|
186
|
+
| `createSocketTransport` | `function` | `sync/socketTransport.ts` |
|
|
187
|
+
| `SocketTransportConfig` | `interface` | `sync/socketTransport.ts` |
|
|
188
|
+
| `DEFAULT_BATCH_SIZE` | `const` | `sync/transport.ts` |
|
|
189
|
+
| `DEFAULT_MUTATION_TIMEOUT_MS` | `const` | `sync/transport.ts` |
|
|
190
|
+
| `SendMutationBatchResult` | `type` | `sync/transport.ts` |
|
|
191
|
+
| `SendMutationResult` | `type` | `sync/transport.ts` |
|
|
192
|
+
| `SyncTransport` | `interface` | `sync/transport.ts` |
|
|
193
|
+
| `TransportStatus` | `interface` | `sync/transport.ts` |
|
|
194
|
+
|
|
195
|
+
### Debug log types
|
|
196
|
+
|
|
197
|
+
| Symbol | Kind | Source |
|
|
198
|
+
|--------|------|--------|
|
|
199
|
+
| `SyncDebugDirection` | `type` | `debug/debugLog.ts:43` |
|
|
200
|
+
| `SyncDebugEvent` | `interface` | `debug/debugLog.ts:46` |
|
|
201
|
+
| `SyncDebugEventType` | `type` | `debug/debugLog.ts:27` |
|
|
202
|
+
| `SyncDebugLog` | `interface` | `debug/debugLog.ts` |
|
|
203
|
+
| `SyncDebugLogOptions` | `interface` | `debug/debugLog.ts` |
|
|
204
|
+
| `SyncDebugRecordInput` | `interface` | `debug/debugLog.ts` |
|
|
205
|
+
| `SyncDebugSnapshot` | `interface` | `debug/debugLog.ts` |
|
|
206
|
+
| `SyncDebugStats` | `interface` | `debug/debugLog.ts` |
|
|
207
|
+
|
|
208
|
+
**Not exported (internal):** `resolveConflict` (mutation writer), `writeConflict`, cursor mutators, IndexedDB helpers, debug broadcast bridge — use `client.resolveConflict` / `useConflicts().resolve` instead.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## `@terreno/syncdb/react` (`syncdb/src/react/index.ts` → hooks + provider)
|
|
213
|
+
|
|
214
|
+
| Symbol | Kind | Description | Source |
|
|
215
|
+
|--------|------|-------------|--------|
|
|
216
|
+
| `SyncDbProvider` | `React.FC<SyncDbProviderProps>` | Provides client to descendants | `provider.tsx:13` |
|
|
217
|
+
| `SyncDbProviderProps` | `interface` `{client, children}` | Provider props | `provider.tsx:7` |
|
|
218
|
+
| `useSyncDbClient` | `() => SyncDb` | Access client from context | `provider.tsx:18` |
|
|
219
|
+
| `useEntity` | `<T>(collection, id) => UseEntityResult<T>` | Subscribe to one entity | `hooks.ts:117` |
|
|
220
|
+
| `UseEntityResult` | `interface` `{data, deleted, seq, isPending}` | `useEntity` return shape | `hooks.ts:105` |
|
|
221
|
+
| `useQuery` | `<T>(collection, options?) => T[]` | Subscribe to collection (decoded data) | `hooks.ts:170` |
|
|
222
|
+
| `UseQueryOptions` | `interface` `{filter?, sort?, includeDeleted?}` | Query options | `hooks.ts:149` |
|
|
223
|
+
| `useEntityIds` | `<T>(collection, options?) => string[]` | Stable id list for virtualization | `hooks.ts:221` |
|
|
224
|
+
| `useMutate` | `(collection) => UseMutateResult` | `{create, update, remove}` | `hooks.ts:266` |
|
|
225
|
+
| `UseMutateResult` | `interface` | Mutation helpers | `hooks.ts:253` |
|
|
226
|
+
| `useSyncStatus` | `() => SyncStatus` | Reactive aggregate status | `hooks.ts:296` |
|
|
227
|
+
| `useConflicts` | `() => UseConflictsResult` | `{conflicts, resolve}` | `hooks.ts:328` |
|
|
228
|
+
| `UseConflictsResult` | `interface` | Conflict hook return | `hooks.ts:320` |
|
|
229
|
+
| `useSyncDebugLog` | `() => UseSyncDebugLogResult` | Debug log subscription | `hooks.ts:379` |
|
|
230
|
+
| `UseSyncDebugLogResult` | `interface` `{enabled, events, stats, log, clear}` | Debug hook return | `hooks.ts:355` |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## `@terreno/syncdb/testing` (`syncdb/src/testing/index.ts`)
|
|
235
|
+
|
|
236
|
+
| Symbol | Kind | Description | Source |
|
|
237
|
+
|--------|------|-------------|--------|
|
|
238
|
+
| `createFakeTransport` | `() => FakeTransport` | In-memory transport double | `testing/fakeTransport.ts:74` |
|
|
239
|
+
| `FakeTransport` | `interface` extends `SyncTransport` | Test controls + recording | `testing/fakeTransport.ts:32` |
|
|
240
|
+
| `FakeMutationResponder` | `type` | Single-mutation responder | `testing/fakeTransport.ts:22` |
|
|
241
|
+
| `FakeBatchResponder` | `type` | Batch responder | `testing/fakeTransport.ts:27` |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Reference migration (`example-frontend`)
|
|
246
|
+
|
|
247
|
+
Before paths are from `origin/master`; after paths are on the current branch.
|
|
248
|
+
|
|
249
|
+
### (a) Store setup
|
|
250
|
+
|
|
251
|
+
| | Path | What changed |
|
|
252
|
+
|---|------|--------------|
|
|
253
|
+
| **Before** | `example-frontend/store/index.ts` (master) | `generateAuthSlice(terrenoApi)`, `createOfflineMiddleware`, Redux persist, RTK Query reducer |
|
|
254
|
+
| **After** | `example-frontend/store/index.ts` | `generateBetterAuthSlice`, RTK only for non-synced API + app state |
|
|
255
|
+
| **After** | `example-frontend/store/syncdb.ts` | `createSyncDb({authProvider: betterAuthAdapter(...), collections: ["todos"], ...})` |
|
|
256
|
+
|
|
257
|
+
### (b) Provider wiring
|
|
258
|
+
|
|
259
|
+
| | Path | What changed |
|
|
260
|
+
|---|------|--------------|
|
|
261
|
+
| **Before** | `example-frontend/app/_layout.tsx` (master) | Redux `Provider` only; todos screen uses RTK hooks directly |
|
|
262
|
+
| **After** | `example-frontend/app/(tabs)/index.tsx` | `SyncDbProvider client={syncDb}` wraps `SyncTodosScreen` |
|
|
263
|
+
| **After** | `example-frontend/app/_layout.tsx` | `syncDb.start()` / `stop()` on login; `SyncDbProvider` + `SyncHealthToast` at root when `userId` set |
|
|
264
|
+
|
|
265
|
+
### (c) Reads
|
|
266
|
+
|
|
267
|
+
| | Path | What changed |
|
|
268
|
+
|---|------|--------------|
|
|
269
|
+
| **Before** | `example-frontend/app/(tabs)/index.tsx` (master) | `useGetTodosQuery({}, {skip: !userId})`, `isLoading`, `refetch` |
|
|
270
|
+
| **After** | `example-frontend/components/SyncTodosScreen.tsx` | `useEntityIds<SyncTodo>("todos", {filter, sort})` + per-row `useEntity("todos", id)` |
|
|
271
|
+
|
|
272
|
+
### (d) Writes
|
|
273
|
+
|
|
274
|
+
| | Path | What changed |
|
|
275
|
+
|---|------|--------------|
|
|
276
|
+
| **Before** | `example-frontend/app/(tabs)/index.tsx` (master) | `usePostTodosMutation`, `usePatchTodosByIdMutation`, `useDeleteTodosByIdMutation`, `.unwrap()`, per-item `isUpdating` spinners |
|
|
277
|
+
| **After** | `example-frontend/components/SyncTodosScreen.tsx` | `useMutate("todos")` → `create({data})`, `update({id, data})`, `remove({id})`; gated on `useSyncDbReady()` |
|
|
278
|
+
|
|
279
|
+
### (e) Conflict UI
|
|
280
|
+
|
|
281
|
+
| | Path | What changed |
|
|
282
|
+
|---|------|--------------|
|
|
283
|
+
| **Before** | master todos screen | `useServerStatus` undismissed conflicts (RTK offline slice) |
|
|
284
|
+
| **After** | `example-frontend/components/SyncConflictsController.tsx` | App-wide conflict sheet controller |
|
|
285
|
+
| **After** | `example-frontend/app/_layout.tsx` | `ConflictSheet` via `SyncHealthToast` `renderConflictsModal` |
|
|
286
|
+
| **After** | `example-frontend/components/SyncTodosScreen.tsx` | `SyncStatusBanner` `onOpenConflicts` → `openConflicts("todos")` |
|
|
287
|
+
|
|
288
|
+
### (f) Sync-status UI
|
|
289
|
+
|
|
290
|
+
| | Path | What changed |
|
|
291
|
+
|---|------|--------------|
|
|
292
|
+
| **Before** | master todos screen | `useServerStatus`, `OfflineBanner`, request `isLoading` / `isFetching` / `RefreshControl` |
|
|
293
|
+
| **After** | `example-frontend/components/SyncTodosScreen.tsx` | `useSyncStatus()` + `SyncStatusBanner` |
|
|
294
|
+
| **After** | `example-frontend/components/SyncHealthToast.tsx` | Persistent toasts for conflicts, failures, backlog |
|
|
295
|
+
| **After** | `example-frontend/components/syncHealthSignals.ts` | Toast signal computation |
|
|
296
|
+
|
|
297
|
+
### Not covered by the reference migration
|
|
298
|
+
|
|
299
|
+
- **Auth migration details** — Better Auth session atom bridge in `store/syncdb.ts`; login still uses `@terreno/rtk` Better Auth slice (documented in migration guide §7).
|
|
300
|
+
- **Feature flags** — OpenFeature bridge in `_layout.tsx` (`OpenFeatureBridge` + `useTerrenoFeatureFlags`); no per-screen flag gating todos (syncdb-only).
|
|
301
|
+
- **Other screens** — Profile, admin, AI explorer, consent flow still use generated RTK hooks (`store/sdk.ts`).
|
|
302
|
+
- **SDK realtime config for todos** — `TODO_REALTIME_ENDPOINTS` in `sdk.ts` remains but is unused by `SyncTodosScreen`.
|
|
303
|
+
- **Debug tooling** — `example-frontend/app/syncdb-debug.tsx` (`useSyncDebugLog`).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Implementation Plan: @terreno/syncdb-codegen
|
|
2
|
+
|
|
3
|
+
**Status:** Draft
|
|
4
|
+
**Branch:** cursor/ip-syncdb-codegen-41d6
|
|
5
|
+
**Owner:**
|
|
6
|
+
**Created:** 2026-07-08
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Ship `@terreno/syncdb-codegen`: a small CLI (runnable as an npm bin under bun and buildable as a standalone compiled bun binary) that reads a backend OpenAPI spec and generates a static, typed set of syncdb hooks — the local-first analogue of the existing `@rtk-query/codegen-openapi` flow (`bun run sdk` → `store/openApiSdk.ts`).
|
|
11
|
+
|
|
12
|
+
Today syncdb consumers hand-write both the entity types and the collection strings:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// example-frontend/components/SyncTodosScreen.tsx — all manual today
|
|
16
|
+
interface SyncTodo { _id: string; title?: string; completed?: boolean; created?: string; }
|
|
17
|
+
const todos = useQuery<SyncTodo>("todos", {sort: sortByCreatedDesc});
|
|
18
|
+
const {update, remove} = useMutate("todos"); // update/remove payloads are Record<string, unknown>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
After this IP, a generated `store/syncDbSdk.ts` provides one hook per operation with the exact names RTK codegen would produce for the same routes — `useGetTodosQuery` (list query), `useGetTodosByIdQuery` (read), `usePostTodosMutation` (create), `usePatchTodosByIdMutation` (update/patch), `useDeleteTodosByIdMutation` (delete) — with payload types derived from the same OpenAPI schemas RTK codegen uses (`Todo`, `CreateTodoBody`, `UpdateTodoBody`), plus a `SYNC_COLLECTIONS` constant that feeds `createSyncDb`. Migrating a screen from RTK to syncdb becomes (mostly) an import-path change. Custom hooks are supported the same way RTK supports custom endpoints: the generated file is never edited, and consumers extend it in a sibling file using the same public factory the generated code uses.
|
|
22
|
+
|
|
23
|
+
## Non-Goals
|
|
24
|
+
|
|
25
|
+
- Covering the whole syncdb surface. Only the five CRUD operations (list query, read, create, update/patch, delete) get generated per-collection hooks. `useSyncStatus`, `useConflicts`, `useSyncDbClient`, and `SyncDbProvider` are global (not per-collection) and stay as direct imports from `@terreno/syncdb/react`.
|
|
26
|
+
- Full RTK return-shape emulation. Hook **names and call sites** match RTK; return shapes are syncdb-native where the semantics differ (no `isLoading`/`error` async state on mutations — writes are synchronous local applies plus a durable outbox; sync state lives in `useSyncStatus`). The mapping is documented in the generated file header and the migration guide.
|
|
27
|
+
- Runtime schema validation of mutation payloads (types are compile-time only, matching RTK codegen).
|
|
28
|
+
- Publishing per-platform compiled binaries to npm or GitHub releases (the compiled binary is a local/CI build target for now; `@terreno/syncdb` itself is not in `publish-on-tag.yml` yet either).
|
|
29
|
+
- Watch mode, incremental generation, or multi-output-file support.
|
|
30
|
+
- Generating Redux integration or migrating non-synced endpoints — per `docs/how-to/migrate-rtk-to-syncdb.md`, custom routes stay on RTK.
|
|
31
|
+
|
|
32
|
+
## Decisions
|
|
33
|
+
|
|
34
|
+
Options were considered where the design was genuinely open; each row records the recommendation this plan is built on. Rejected alternatives are listed so review can overrule them cheaply.
|
|
35
|
+
|
|
36
|
+
| Question | Decision |
|
|
37
|
+
|----------|----------|
|
|
38
|
+
| **How does codegen know which collections are synced?** OpenAPI does not mark sync today; the server-side `syncRegistry` (`api/src/sync/registry.ts`) is in-memory only. Options: (a) emit an `x-terreno-sync` vendor extension on the list path from `modelRouter` when `options.sync` is set; (b) require an explicit `--collections` CLI arg; (c) add a `GET /sync/config` manifest endpoint. | **(a) with (b) as override.** The extension is a ~10-line change in `@terreno/api`'s list OpenAPI middleware, keeps the spec the single input (same as RTK codegen), and can't drift from the backend. `--collections` remains as an explicit filter/override for apps that sync a subset. (c) adds server surface for no extra information and is rejected. |
|
|
39
|
+
| **How is "retries vs not" expressed?** RTK disables retries per-endpoint via `extraOptions: {maxRetries: 0}` (see `rtk/src/authSlice.ts`). syncdb retries live in the replay engine: `MAX_ERROR_NACK_ATTEMPTS = 5` with exponential backoff in `syncdb/src/sync/replayCoordinator.ts`, not configurable per mutation. Options: (a) plumb an optional per-mutation `maxAttempts` through `client.mutate` → outbox row → replay coordinator, and expose `retries` in the codegen config; (b) codegen-only option that is silently ignored until the engine supports it. | **(a).** Small, contained change: `MutateArgs.maxAttempts?`, one optional outbox cell, one `??` in the replay coordinator's error-nack branch. `retries: false` maps to `maxAttempts: 1` (fail fast, like RTK's `maxRetries: 0`), a number maps directly, and omitted keeps the engine default of 5. (b) would generate config that lies. Note syncdb mutations are idempotent via `mutationId`, so unlike RTK the motivation is fail-fast UX (surface a terminal failure immediately), not duplicate-write safety. |
|
|
40
|
+
| **Config surface: flags only, or a config file?** The binary should "take a few args", but per-collection overrides (retries, exclusions) don't map well to flags. | **Flags for the common path, optional JSON config for overrides.** `--schema`, `--out`, `--collections` cover the RTK-equivalent happy path. `--config ./syncdb-codegen.json` adds `{overrides: {<collection>: {retries?: boolean \| number}}, exportName?, sdkImportPath?}`. JSON (not TS) because a compiled bun binary cannot reliably import arbitrary consumer TypeScript at runtime; RTK's TS `ConfigFile` needs the `tsx`/`TS_NODE_PROJECT` dance we currently work around in `example-frontend/scripts/generate-sdk.ts`. |
|
|
41
|
+
| **How are TS types emitted from OpenAPI schemas?** Options: (a) a small hand-rolled emitter for the subset `mongoose-to-swagger` actually produces (objects, primitives, enums as string unions, arrays, `$ref`); (b) depend on `openapi-typescript` or oazapfts. | **(a).** The @terreno/api spec is a narrow, known dialect; a ~150-line emitter with snapshot tests keeps the binary dependency-free and small. If the emitter grows past that dialect, revisit (b). |
|
|
42
|
+
| **Where does the shared hook factory live?** Generated code and custom hooks must be built the same way (the RTK parallel: generated `injectEndpoints` and app `sdk.ts` both build on `emptySplitApi`). Options: (a) new `createCollectionHooks` export in `@terreno/syncdb/react`; (b) inline the factory into every generated file; (c) a runtime export of `@terreno/syncdb-codegen`. | **(a).** One implementation, versioned with the hooks it wraps, and consumers already depend on `@terreno/syncdb`. (b) duplicates logic into generated output; (c) would make the codegen package a runtime dependency of apps, which it should never be. |
|
|
43
|
+
| **Hook granularity and naming.** Options: (a) one grouped `useTodosMutate()` returning `{create, update, remove}`; (b) one hook per operation with RTK codegen's exact naming scheme (`use{Method}{Path}Query/Mutation`); (c) per-operation hooks with friendlier names (`useCreateTodo`). | **(b) — per-operation, RTK names.** Generated per collection: `useGetTodosQuery`, `useGetTodosByIdQuery`, `usePostTodosMutation`, `usePatchTodosByIdMutation`, `useDeleteTodosByIdMutation`. Name derivation is the same deterministic pascal-case of the route path RTK uses (no singularization heuristics). This makes RTK→syncdb migration an import swap. Two consequences are accepted: (1) name **collisions with `openApiSdk.ts`** are intentional — during migration both files export e.g. `useGetTodosQuery`, and the import path picks the data source; imports must never be mixed via a barrel that re-exports both. (2) "update" and "patch" are the **same hook**: modelRouter only exposes PATCH (merge semantics) and syncdb's update is a merge, so `usePatchTodosByIdMutation` is the update hook — there is no separate PUT/replace hook to generate. |
|
|
44
|
+
| **Mutation hook return shape.** RTK mutation hooks return `[trigger, {isLoading, error, ...}]` and triggers return promises with `.unwrap()`. syncdb writes are synchronous local applies; there is no per-call async state. | **One-element tuple `[trigger]`.** Keeps the RTK call site (`const [createTodo] = usePostTodosMutation()`) working unchanged; the trigger returns `{mutationId, id}` synchronously instead of a promise, so `.unwrap()`/`isLoading` usages are deleted during migration (the compiler flags them). Pending/conflict state is per-entity (`isPending` on reads) and global (`useSyncStatus`), not per-call. |
|
|
45
|
+
| **Distribution.** "Compiled bun binary" vs npm reality. | **Both, npm-bin first.** The package ships `"bin": {"terreno-syncdb-codegen": "./dist/cli.js"}` with a `#!/usr/bin/env bun` shebang (the `@terreno/mcp` pattern), plus a `build:binary` script running `bun build --compile src/cli.ts --outfile dist/terreno-syncdb-codegen` (the `example-backend/Dockerfile` pattern) for standalone use. Publishing platform binaries is future work. |
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
example-backend example-frontend
|
|
51
|
+
modelRouter("/todos", Todo, {sync: {...}}) bun run sync-sdk
|
|
52
|
+
└─ list OpenAPI middleware emits └─ terreno-syncdb-codegen
|
|
53
|
+
x-terreno-sync on GET /todos --schema http://localhost:4000/openapi.json
|
|
54
|
+
│ --out ./store/syncDbSdk.ts
|
|
55
|
+
▼ │
|
|
56
|
+
/openapi.json ──── fetch/read ────────────────────────┘
|
|
57
|
+
│ emits
|
|
58
|
+
▼
|
|
59
|
+
store/syncDbSdk.ts (generated, never edited)
|
|
60
|
+
├─ interface Todo / CreateTodoBody / UpdateTodoBody
|
|
61
|
+
├─ export const SYNC_COLLECTIONS = ["todos"] as const
|
|
62
|
+
└─ export const {useGetTodosQuery, useGetTodosByIdQuery,
|
|
63
|
+
usePostTodosMutation, usePatchTodosByIdMutation,
|
|
64
|
+
useDeleteTodosByIdMutation} =
|
|
65
|
+
createCollectionHooks<...>({collection: "todos"})
|
|
66
|
+
│ extended by (custom hooks)
|
|
67
|
+
▼
|
|
68
|
+
store/syncSdk.ts (hand-written, optional)
|
|
69
|
+
└─ createCollectionHooks<LocalNote>({collection: "notes"})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Three packages change:
|
|
73
|
+
|
|
74
|
+
1. **`@terreno/syncdb`** (runtime): `createCollectionHooks` factory in `src/react/`, plus per-mutation `maxAttempts` plumbing through `client.mutate` → outbox → replay coordinator.
|
|
75
|
+
2. **`@terreno/api`**: `listOpenApiMiddleware` (or the shared path-building step in `api/src/openApi.ts`) adds `"x-terreno-sync": {collection, scope}` to the list operation when `options.sync` is present.
|
|
76
|
+
3. **`@terreno/syncdb-codegen`** (new): CLI that parses the spec, discovers synced collections, and emits the SDK file.
|
|
77
|
+
|
|
78
|
+
### Runtime factory (`@terreno/syncdb/react`)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
export interface CollectionHooksConfig {
|
|
82
|
+
collection: string;
|
|
83
|
+
/** false → 1 attempt (fail fast); number → max replay attempts; omitted → engine default (5). */
|
|
84
|
+
retries?: boolean | number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type MutationTrigger<TArgs> = (args: TArgs) => {mutationId: string; id: string};
|
|
88
|
+
|
|
89
|
+
export interface CollectionHooks<TData, TCreate, TUpdate> {
|
|
90
|
+
/** List query (RTK: useGet{Path}Query). */
|
|
91
|
+
useListQuery: (options?: UseQueryOptions<TData>) => {data: TData[]};
|
|
92
|
+
/** Single-entity read (RTK: useGet{Path}ByIdQuery). */
|
|
93
|
+
useReadQuery: (id: string) => UseEntityResult<TData>;
|
|
94
|
+
/** Create (RTK: usePost{Path}Mutation). */
|
|
95
|
+
useCreateMutation: () => [MutationTrigger<{data: TCreate}>];
|
|
96
|
+
/** Update via merge — patch semantics (RTK: usePatch{Path}ByIdMutation). */
|
|
97
|
+
useUpdateMutation: () => [MutationTrigger<{id: string; data: TUpdate}>];
|
|
98
|
+
/** Soft delete (RTK: useDelete{Path}ByIdMutation). */
|
|
99
|
+
useDeleteMutation: () => [MutationTrigger<{id: string}>];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const createCollectionHooks = <
|
|
103
|
+
TData = Record<string, unknown>,
|
|
104
|
+
TCreate = Record<string, unknown>,
|
|
105
|
+
TUpdate = Partial<TCreate>,
|
|
106
|
+
>(config: CollectionHooksConfig): CollectionHooks<TData, TCreate, TUpdate> => { ... };
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The factory returns **operation-named keys** (`useListQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`); the generated file renames them to the RTK-style collection-specific names via destructuring (`useListQuery: useGetTodosQuery`, ...). Custom hooks can keep the generic names or rename the same way. Each hook delegates to the existing `useQuery`/`useEntity`/`useMutate` internals (no listener logic duplicated) and threads `retries` into `client.mutate` as `maxAttempts`. This is the `emptySplitApi` of the syncdb world: generated code calls it, and custom hooks call it directly — that is the whole "custom hooks the same way RTK does" story.
|
|
110
|
+
|
|
111
|
+
### Generated file shape
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// store/syncDbSdk.ts — GENERATED by @terreno/syncdb-codegen. Do not edit.
|
|
115
|
+
import {createCollectionHooks} from "@terreno/syncdb/react";
|
|
116
|
+
|
|
117
|
+
export interface Todo { _id: string; title: string; completed: boolean; ownerId: string; created: string; updated: string; }
|
|
118
|
+
export interface CreateTodoBody { title: string; completed?: boolean; }
|
|
119
|
+
export type UpdateTodoBody = Partial<CreateTodoBody>;
|
|
120
|
+
|
|
121
|
+
/** Collections with sync enabled on the backend; pass to createSyncDb. */
|
|
122
|
+
export const SYNC_COLLECTIONS = ["todos"] as const;
|
|
123
|
+
|
|
124
|
+
export const {
|
|
125
|
+
useListQuery: useGetTodosQuery,
|
|
126
|
+
useReadQuery: useGetTodosByIdQuery,
|
|
127
|
+
useCreateMutation: usePostTodosMutation,
|
|
128
|
+
useUpdateMutation: usePatchTodosByIdMutation,
|
|
129
|
+
useDeleteMutation: useDeleteTodosByIdMutation,
|
|
130
|
+
} = createCollectionHooks<Todo, CreateTodoBody, UpdateTodoBody>({collection: "todos"});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Call-site comparison for migration:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// RTK // syncdb (generated)
|
|
137
|
+
const {data} = useGetTodosQuery({}); const {data} = useGetTodosQuery();
|
|
138
|
+
const {data} = useGetTodosByIdQuery({id}); const {data, isPending} = useGetTodosByIdQuery(id);
|
|
139
|
+
const [createTodo] = usePostTodosMutation(); const [createTodo] = usePostTodosMutation();
|
|
140
|
+
await createTodo({body}).unwrap(); createTodo({data}); // sync, returns {mutationId, id}
|
|
141
|
+
const [patchTodo] = usePatchTodosByIdMutation(); const [patchTodo] = usePatchTodosByIdMutation();
|
|
142
|
+
const [deleteTodo] = useDeleteTodosByIdMutation(); const [deleteTodo] = useDeleteTodosByIdMutation();
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`SYNC_COLLECTIONS` replaces the hand-maintained list in `example-frontend/store/syncdb.ts`, closing the "client string must match backend route" gap.
|
|
146
|
+
|
|
147
|
+
### CLI
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
terreno-syncdb-codegen --schema <url|path> --out <file> [--collections a,b] [--config <json>] [--no-format]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `--schema` (required): OpenAPI URL or local JSON file (same source as `openapi-config.ts`'s `schemaFile`).
|
|
154
|
+
- `--out` (required): output `.ts` path.
|
|
155
|
+
- `--collections` (optional): comma-separated allowlist; also the fallback when the spec has no `x-terreno-sync` extensions (older backends). Errors clearly if neither extensions nor the flag identify any collection.
|
|
156
|
+
- `--config` (optional): JSON file with `{overrides: {todos: {retries: false}}}` per-collection settings.
|
|
157
|
+
- `--no-format` (optional): skip the biome/prettier formatting pass (codegen formats output via `Bun.spawn`-ed `bunx biome check --write` when available, mirroring `generate-sdk.ts`; falls back to raw emit).
|
|
158
|
+
|
|
159
|
+
## Models
|
|
160
|
+
|
|
161
|
+
No Mongoose model changes. One local-storage schema change in syncdb: the `_outbox` reserved table gains an optional `maxAttempts` numeric cell (`syncdb/src/storage/schema.ts`, `syncdb/src/mutations/outbox.ts`). Missing cell means engine default, so existing persisted stores need no migration.
|
|
162
|
+
|
|
163
|
+
## APIs
|
|
164
|
+
|
|
165
|
+
No new HTTP endpoints. One OpenAPI spec addition in `@terreno/api`: list operations of sync-enabled model routers gain
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"x-terreno-sync": {"collection": "todos", "scope": "owner"}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Vendor extensions are legal OpenAPI and ignored by the RTK codegen, Swagger UI, and the AJV validator, so this is backward-compatible.
|
|
172
|
+
|
|
173
|
+
## Notifications
|
|
174
|
+
|
|
175
|
+
None needed.
|
|
176
|
+
|
|
177
|
+
## UI
|
|
178
|
+
|
|
179
|
+
No new screens. `example-frontend/components/SyncTodosScreen.tsx` swaps its hand-rolled `SyncTodo` interface and string-based hooks for the generated `useGetTodosQuery`/`usePostTodosMutation`/`usePatchTodosByIdMutation`/`useDeleteTodosByIdMutation`, and `store/syncdb.ts` imports `SYNC_COLLECTIONS` from the generated file. Behavior is unchanged; the existing syncdb Playwright e2e suite is the regression net.
|
|
180
|
+
|
|
181
|
+
## Phases
|
|
182
|
+
|
|
183
|
+
Single PR is plausible but the work splits cleanly into four phases if review prefers smaller units:
|
|
184
|
+
|
|
185
|
+
1. **syncdb runtime** — `createCollectionHooks` + `maxAttempts` plumbing (mutate → outbox → replay coordinator), with unit tests. No consumer-visible breaking changes.
|
|
186
|
+
2. **api spec extension** — `x-terreno-sync` on list operations, with an OpenAPI generation test asserting the extension appears for `example-backend`-style sync configs and is absent otherwise.
|
|
187
|
+
3. **codegen package** — new `syncdb-codegen/` workspace package: arg parsing, spec loading, collection discovery, type emitter, file emitter, snapshot tests against a fixture spec captured from `example-backend`, `build:binary` script.
|
|
188
|
+
4. **integration + docs** — `example-frontend` `sync-sdk` script and screen migration; README updates (`syncdb/README.md`, new `syncdb-codegen/README.md`, `docs/how-to/migrate-rtk-to-syncdb.md` note); root `package.json` convenience scripts.
|
|
189
|
+
|
|
190
|
+
## Feature Flags & Migrations
|
|
191
|
+
|
|
192
|
+
- No new feature flags. The existing `use-syncdb` flag continues to gate the syncdb path in `example-frontend`.
|
|
193
|
+
- No data migrations. The new `_outbox.maxAttempts` cell is optional and defaulted.
|
|
194
|
+
- Publishing: not wired into `publish-on-tag.yml` in this IP (neither is `@terreno/syncdb` yet); tracked in Future Work so both are added together.
|
|
195
|
+
|
|
196
|
+
## Activity Log & User Updates
|
|
197
|
+
|
|
198
|
+
None — developer tooling only.
|
|
199
|
+
|
|
200
|
+
## Not Included / Future Work
|
|
201
|
+
|
|
202
|
+
- Publishing `@terreno/syncdb` and `@terreno/syncdb-codegen` to npm (add `publish-syncdb` + `publish-syncdb-codegen` jobs to `publish-on-tag.yml`).
|
|
203
|
+
- Per-platform prebuilt binaries as release artifacts.
|
|
204
|
+
- Generated wrappers for `useSyncStatus`/`useConflicts` (no per-collection typing to add).
|
|
205
|
+
- Server-driven filters/sorts in `useQuery` options (syncdb filters run client-side in JS; nothing for codegen to type beyond `TData`).
|
|
206
|
+
- Zod/AJV runtime validation of mutation payloads against the OpenAPI schema.
|
|
207
|
+
- Custom hook naming via config (`overrides.todos.hookPrefix`), for apps that need to avoid the intentional name overlap with RTK-generated hooks.
|
|
208
|
+
- RTK-compatible async mutation state (`isLoading`, promise-returning triggers with `.unwrap()`), should a drop-in compatibility mode ever be wanted.
|
|
209
|
+
- A `--watch` mode.
|
|
210
|
+
|
|
211
|
+
## Files to Create / Modify
|
|
212
|
+
|
|
213
|
+
**Create**
|
|
214
|
+
- `syncdb-codegen/package.json` — name `@terreno/syncdb-codegen`, `bin`, `build:binary`, catalog deps (dev-only: typescript, biome).
|
|
215
|
+
- `syncdb-codegen/tsconfig.json`, `syncdb-codegen/biome.jsonc` — copied from `syncdb/` siblings.
|
|
216
|
+
- `syncdb-codegen/src/cli.ts` — `#!/usr/bin/env bun` entry; arg parsing (`Bun.argv` + `util.parseArgs`), exit codes, error messages.
|
|
217
|
+
- `syncdb-codegen/src/loadSpec.ts` — fetch/read + JSON parse of the OpenAPI document.
|
|
218
|
+
- `syncdb-codegen/src/discoverCollections.ts` — walk `paths`, collect `x-terreno-sync` operations, resolve entity/create/update schema refs from the list/create/patch operations; apply `--collections` filter/fallback.
|
|
219
|
+
- `syncdb-codegen/src/emitTypes.ts` — OpenAPI schema subset → TS interfaces.
|
|
220
|
+
- `syncdb-codegen/src/emitSdk.ts` — assemble the output file (header, types, `SYNC_COLLECTIONS`, `createCollectionHooks` calls with overrides applied).
|
|
221
|
+
- `syncdb-codegen/src/*.test.ts` — unit + snapshot tests against `syncdb-codegen/src/fixtures/openapi.example.json`.
|
|
222
|
+
- `syncdb-codegen/README.md` — usage, args, config format, custom-hooks pattern.
|
|
223
|
+
- `example-frontend/syncdb-codegen.json` — per-collection overrides example (todos with default retries).
|
|
224
|
+
|
|
225
|
+
**Modify**
|
|
226
|
+
- `syncdb/src/react/collectionHooks.ts` (new file in existing package) + `syncdb/src/react/index.ts` — `createCollectionHooks` export.
|
|
227
|
+
- `syncdb/src/client.ts` — `MutateArgs.maxAttempts?`, pass-through to outbox.
|
|
228
|
+
- `syncdb/src/mutations/outbox.ts`, `syncdb/src/storage/schema.ts` — optional `maxAttempts` cell.
|
|
229
|
+
- `syncdb/src/sync/replayCoordinator.ts` — `row.maxAttempts ?? MAX_ERROR_NACK_ATTEMPTS` in the error-nack branch.
|
|
230
|
+
- `api/src/openApi.ts` (`listOpenApiMiddleware`) — emit `x-terreno-sync` when `options.sync` is set.
|
|
231
|
+
- `example-frontend/package.json` — `"sync-sdk": "terreno-syncdb-codegen --schema ... --out ./store/syncDbSdk.ts --config ./syncdb-codegen.json"` script; add `@terreno/syncdb-codegen` as a workspace dev dependency.
|
|
232
|
+
- `example-frontend/store/syncDbSdk.ts` — generated output (checked in, like `openApiSdk.ts`).
|
|
233
|
+
- `example-frontend/store/syncdb.ts` — import `SYNC_COLLECTIONS` from the generated file.
|
|
234
|
+
- `example-frontend/components/SyncTodosScreen.tsx` — use generated hooks; delete the local `SyncTodo` interface.
|
|
235
|
+
- Root `package.json` — workspace entry + `syncdb-codegen:compile` / `syncdb-codegen:test` scripts.
|
|
236
|
+
- `syncdb/README.md`, `docs/how-to/migrate-rtk-to-syncdb.md` — codegen sections.
|
|
237
|
+
|
|
238
|
+
## Task List
|
|
239
|
+
|
|
240
|
+
See [docs/tasks/syncdb-codegen.md](../tasks/syncdb-codegen.md).
|
|
241
|
+
|
|
242
|
+
## Acceptance Criteria
|
|
243
|
+
|
|
244
|
+
- [ ] `createCollectionHooks<Todo>({collection: "todos"})` returns the five per-operation hooks (`useListQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`) whose behavior matches direct `useQuery`/`useEntity`/`useMutate` calls; mutation hooks return `[trigger]` tuples so RTK-style destructuring compiles (unit tests in `syncdb/src/react/`).
|
|
245
|
+
- [ ] `createCollectionHooks({collection: "todos", retries: false})` produces mutations whose outbox rows fail terminally after a single `error` nack; `retries: 3` fails after three attempts; omitted keeps the current 5-attempt backoff (replay coordinator tests).
|
|
246
|
+
- [ ] `GET /openapi.json` from a backend with `sync: {scope: {type: "owner"}}` on `/todos` includes `x-terreno-sync: {collection: "todos", scope: "owner"}` on the list operation; non-synced routes have no extension (api test).
|
|
247
|
+
- [ ] `terreno-syncdb-codegen --schema <fixture> --out out.ts` emits a file that type-checks under the example-frontend tsconfig and contains `SYNC_COLLECTIONS`, typed interfaces, and per synced collection the five RTK-named hooks (`useGetTodosQuery`, `useGetTodosByIdQuery`, `usePostTodosMutation`, `usePatchTodosByIdMutation`, `useDeleteTodosByIdMutation`) — names byte-identical to what `@rtk-query/codegen-openapi` derives for the same routes (snapshot test).
|
|
248
|
+
- [ ] `--collections todos` filters output to that collection; a spec without extensions and no `--collections` flag exits non-zero with an actionable message.
|
|
249
|
+
- [ ] A config file with `{overrides: {todos: {retries: false}}}` produces `createCollectionHooks<...>({collection: "todos", retries: false})` in the output.
|
|
250
|
+
- [ ] `bun run build:binary` in `syncdb-codegen/` produces a standalone executable that generates identical output to the bin entry.
|
|
251
|
+
- [ ] `bun run sync-sdk` in `example-frontend` (backend running) regenerates `store/syncDbSdk.ts` with no diff when the backend is unchanged; SyncTodosScreen works against the generated hooks (existing syncdb e2e suite passes).
|
|
252
|
+
- [ ] `bun run lint`, `bun run syncdb:test`, `bun run api:test`, and the new `syncdb-codegen:test` pass.
|