Skip to content

Commit 15e981f

Browse files
Merge remote-tracking branch 'upstream/main' into powersync
2 parents fe165e5 + 48b8e8f commit 15e981f

File tree

82 files changed

+7145
-1086
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+7145
-1086
lines changed

.changeset/in-memory-fallback-for-ssr.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

.github/workflows/pr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ jobs:
5454
repo-token: "${{ secrets.GITHUB_TOKEN }}"
5555
pattern: "./packages/db/dist/**/*.{js,mjs}"
5656
comment-key: "db-package-size"
57+
build-script: "build:minified"
5758
- name: Compressed Size Action - React DB Package
5859
uses: preactjs/compressed-size-action@v2
5960
with:
6061
repo-token: "${{ secrets.GITHUB_TOKEN }}"
6162
pattern: "./packages/react-db/dist/**/*.{js,mjs}"
6263
comment-key: "react-db-package-size"
64+
build-script: "build:minified"
6365
build-example:
6466
name: Build Example Site
6567
runs-on: ubuntu-latest

SERIALIZED_TRANSACTION_PLAN.md

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
# Implementation Plan for `useSerializedTransaction` with TanStack Pacer
2+
3+
Based on [GitHub issue #35](https://github.com/TanStack/db/issues/35), using @tanstack/pacer for strategy implementation across all 5 framework integrations.
4+
5+
## Overview
6+
7+
Create a framework-agnostic core in `@tanstack/db` that manages optimistic transactions with pluggable queuing strategies powered by TanStack Pacer. Each framework package wraps the core with framework-specific reactive primitives.
8+
9+
## Architecture Pattern
10+
11+
The core transaction logic stays in one place (`@tanstack/db`) while each framework provides its own wrapper using framework-specific reactive primitives.
12+
13+
```typescript
14+
// Core in @tanstack/db (framework-agnostic)
15+
createSerializedTransaction(config) // Returns { mutate, cleanup }
16+
17+
// React wrapper
18+
useSerializedTransaction(config) // Uses React hooks, returns mutate function
19+
20+
// Solid wrapper
21+
useSerializedTransaction(config) // Uses Solid signals, matches useLiveQuery pattern
22+
23+
// Svelte/Vue wrappers
24+
useSerializedTransaction(config) // Framework-specific implementations
25+
26+
// Angular wrapper
27+
injectSerializedTransaction(config) // Uses Angular DI, follows injectLiveQuery pattern
28+
```
29+
30+
## Available Strategies (Based on Pacer Utilities)
31+
32+
### 1. **debounceStrategy({ wait, leading?, trailing? })**
33+
34+
- Uses Pacer's `Debouncer` class
35+
- Waits for pause in activity before committing
36+
- **Best for:** Search inputs, auto-save fields
37+
38+
### 2. **queueStrategy({ wait?, maxSize?, addItemsTo?, getItemsFrom? })**
39+
40+
- Uses Pacer's `Queuer` class
41+
- Processes all transactions in order (FIFO/LIFO)
42+
- FIFO: `{ addItemsTo: 'back', getItemsFrom: 'front' }`
43+
- LIFO: `{ addItemsTo: 'back', getItemsFrom: 'back' }`
44+
- **Best for:** Sequential operations that must all complete
45+
46+
### 3. **throttleStrategy({ wait, leading?, trailing? })**
47+
48+
- Uses Pacer's `Throttler` class
49+
- Evenly spaces transaction executions over time
50+
- **Best for:** Sliders, scroll handlers, progress bars
51+
52+
### 4. **batchStrategy({ maxSize?, wait?, getShouldExecute? })**
53+
54+
- Uses Pacer's `Batcher` class
55+
- Groups multiple mutations into batches
56+
- Triggers on size or time threshold
57+
- **Best for:** Bulk operations, reducing network calls
58+
59+
## File Structure
60+
61+
```
62+
packages/db/src/
63+
├── serialized-transaction.ts # Core framework-agnostic logic
64+
└── strategies/
65+
├── index.ts # Export all strategies
66+
├── debounceStrategy.ts # Wraps Pacer Debouncer
67+
├── queueStrategy.ts # Wraps Pacer Queuer
68+
├── throttleStrategy.ts # Wraps Pacer Throttler
69+
├── batchStrategy.ts # Wraps Pacer Batcher
70+
└── types.ts # Strategy type definitions
71+
72+
packages/db/package.json # Add @tanstack/pacer dependency
73+
74+
packages/react-db/src/
75+
└── useSerializedTransaction.ts # React hook wrapper
76+
77+
packages/solid-db/src/
78+
└── useSerializedTransaction.ts # Solid wrapper (matches useLiveQuery pattern)
79+
80+
packages/svelte-db/src/
81+
└── useSerializedTransaction.svelte.ts # Svelte wrapper
82+
83+
packages/vue-db/src/
84+
└── useSerializedTransaction.ts # Vue wrapper
85+
86+
packages/angular-db/src/
87+
└── injectSerializedTransaction.ts # Angular wrapper (DI pattern)
88+
89+
packages/*/tests/
90+
└── serialized-transaction.test.ts # Tests per package
91+
```
92+
93+
## Core API Design
94+
95+
```typescript
96+
// Framework-agnostic core (packages/db)
97+
import { debounceStrategy } from '@tanstack/db'
98+
99+
const { mutate, cleanup } = createSerializedTransaction({
100+
mutationFn: async ({ transaction }) => {
101+
await api.save(transaction.mutations)
102+
},
103+
strategy: debounceStrategy({ wait: 500 }),
104+
metadata?: Record<string, unknown>,
105+
})
106+
107+
// mutate() executes mutations according to strategy and returns Transaction
108+
const transaction = mutate(() => {
109+
collection.update(id, draft => { draft.value = newValue })
110+
})
111+
112+
// Await persistence and handle errors
113+
try {
114+
await transaction.isPersisted.promise
115+
console.log('Transaction committed successfully')
116+
} catch (error) {
117+
console.error('Transaction failed:', error)
118+
}
119+
120+
// cleanup() when done (frameworks handle this automatically)
121+
cleanup()
122+
```
123+
124+
## React Hook Wrapper
125+
126+
```typescript
127+
// packages/react-db
128+
import { debounceStrategy } from "@tanstack/react-db"
129+
130+
const mutate = useSerializedTransaction({
131+
mutationFn: async ({ transaction }) => {
132+
await api.save(transaction.mutations)
133+
},
134+
strategy: debounceStrategy({ wait: 1000 }),
135+
})
136+
137+
// Usage in component
138+
const handleChange = async (value) => {
139+
const tx = mutate(() => {
140+
collection.update(id, (draft) => {
141+
draft.value = value
142+
})
143+
})
144+
145+
// Optional: await persistence or handle errors
146+
try {
147+
await tx.isPersisted.promise
148+
} catch (error) {
149+
console.error("Update failed:", error)
150+
}
151+
}
152+
```
153+
154+
## Example: Slider with Different Strategies
155+
156+
```typescript
157+
// Debounce - wait for user to stop moving slider
158+
const mutate = useSerializedTransaction({
159+
mutationFn: async ({ transaction }) => {
160+
await api.updateVolume(transaction.mutations)
161+
},
162+
strategy: debounceStrategy({ wait: 500 }),
163+
})
164+
165+
// Throttle - update every 200ms while sliding
166+
const mutate = useSerializedTransaction({
167+
mutationFn: async ({ transaction }) => {
168+
await api.updateVolume(transaction.mutations)
169+
},
170+
strategy: throttleStrategy({ wait: 200 }),
171+
})
172+
173+
// Debounce with leading/trailing - save first + final value only
174+
const mutate = useSerializedTransaction({
175+
mutationFn: async ({ transaction }) => {
176+
await api.updateVolume(transaction.mutations)
177+
},
178+
strategy: debounceStrategy({ wait: 0, leading: true, trailing: true }),
179+
})
180+
181+
// Queue - save every change in order (FIFO)
182+
const mutate = useSerializedTransaction({
183+
mutationFn: async ({ transaction }) => {
184+
await api.updateVolume(transaction.mutations)
185+
},
186+
strategy: queueStrategy({
187+
wait: 200,
188+
addItemsTo: "back",
189+
getItemsFrom: "front",
190+
}),
191+
})
192+
```
193+
194+
## Implementation Steps
195+
196+
### Phase 1: Core Package (@tanstack/db)
197+
198+
1. Add `@tanstack/pacer` dependency to packages/db/package.json
199+
2. Create strategy type definitions in strategies/types.ts
200+
3. Implement strategy factories:
201+
- `debounceStrategy.ts` - wraps Pacer Debouncer
202+
- `queueStrategy.ts` - wraps Pacer Queuer
203+
- `throttleStrategy.ts` - wraps Pacer Throttler
204+
- `batchStrategy.ts` - wraps Pacer Batcher
205+
4. Create core `createSerializedTransaction()` function
206+
5. Export strategies + core function from packages/db/src/index.ts
207+
208+
### Phase 2: Framework Wrappers
209+
210+
6. **React** - Create `useSerializedTransaction` using useRef/useEffect/useCallback
211+
7. **Solid** - Create `useSerializedTransaction` using createSignal/onCleanup (matches `useLiveQuery` pattern)
212+
8. **Svelte** - Create `useSerializedTransaction` using Svelte stores
213+
9. **Vue** - Create `useSerializedTransaction` using ref/onUnmounted
214+
10. **Angular** - Create `injectSerializedTransaction` using inject/DestroyRef (matches `injectLiveQuery` pattern)
215+
216+
### Phase 3: Testing & Documentation
217+
218+
11. Write tests for core logic in packages/db
219+
12. Write tests for each framework wrapper
220+
13. Update README with examples
221+
14. Add TypeScript examples to docs
222+
223+
## Strategy Type System
224+
225+
```typescript
226+
export type Strategy =
227+
| DebounceStrategy
228+
| QueueStrategy
229+
| ThrottleStrategy
230+
| BatchStrategy
231+
232+
interface BaseStrategy<TName extends string = string> {
233+
_type: TName // Discriminator for type narrowing
234+
execute: (fn: () => void) => void | Promise<void>
235+
cleanup: () => void
236+
}
237+
238+
export function debounceStrategy(opts: {
239+
wait: number
240+
leading?: boolean
241+
trailing?: boolean
242+
}): DebounceStrategy
243+
244+
export function queueStrategy(opts?: {
245+
wait?: number
246+
maxSize?: number
247+
addItemsTo?: "front" | "back"
248+
getItemsFrom?: "front" | "back"
249+
}): QueueStrategy
250+
251+
export function throttleStrategy(opts: {
252+
wait: number
253+
leading?: boolean
254+
trailing?: boolean
255+
}): ThrottleStrategy
256+
257+
export function batchStrategy(opts?: {
258+
maxSize?: number
259+
wait?: number
260+
getShouldExecute?: (items: any[]) => boolean
261+
}): BatchStrategy
262+
```
263+
264+
## Technical Implementation Details
265+
266+
### Core createSerializedTransaction
267+
268+
The core function will:
269+
270+
1. Accept a strategy and mutationFn
271+
2. Create a wrapper around `createTransaction` from existing code
272+
3. Use the strategy's `execute()` method to control when transactions are committed
273+
4. Return `{ mutate, cleanup }` where:
274+
- `mutate(callback): Transaction` - executes mutations according to strategy and returns the Transaction object
275+
- `cleanup()` - cleans up strategy resources
276+
277+
**Important:** The `mutate()` function returns a `Transaction` object so callers can:
278+
279+
- Await `transaction.isPersisted.promise` to know when persistence completes
280+
- Handle errors via try/catch or `.catch()`
281+
- Access transaction state and metadata
282+
283+
### Strategy Factories
284+
285+
Each strategy factory returns an object with:
286+
287+
- `execute(fn)` - wraps the function with Pacer's utility
288+
- `cleanup()` - cleans up the Pacer instance
289+
290+
Example for debounceStrategy:
291+
292+
```typescript
293+
// NOTE: Import path needs validation - Pacer may export from main entry point
294+
// Likely: import { Debouncer } from '@tanstack/pacer' or similar
295+
import { Debouncer } from "@tanstack/pacer" // TODO: Validate actual export path
296+
297+
export function debounceStrategy(opts: {
298+
wait: number
299+
leading?: boolean
300+
trailing?: boolean
301+
}) {
302+
const debouncer = new Debouncer(opts)
303+
304+
return {
305+
_type: "debounce" as const,
306+
execute: (fn: () => void) => {
307+
debouncer.execute(fn)
308+
},
309+
cleanup: () => {
310+
debouncer.cancel()
311+
},
312+
}
313+
}
314+
```
315+
316+
### React Hook Implementation
317+
318+
```typescript
319+
export function useSerializedTransaction(config) {
320+
// Include strategy in dependencies to handle strategy changes
321+
const { mutate, cleanup } = useMemo(() => {
322+
return createSerializedTransaction(config)
323+
}, [config.mutationFn, config.metadata, config.strategy])
324+
325+
// Cleanup on unmount or when dependencies change
326+
useEffect(() => {
327+
return () => cleanup()
328+
}, [cleanup])
329+
330+
// Use useCallback to provide stable reference
331+
const stableMutate = useCallback(mutate, [mutate])
332+
333+
return stableMutate
334+
}
335+
```
336+
337+
**Key fixes:**
338+
339+
- Include `config.strategy` in `useMemo` dependencies to handle strategy changes
340+
- Properly cleanup when strategy changes (via useEffect cleanup)
341+
- Return stable callback reference via `useCallback`
342+
343+
## Benefits
344+
345+
-Leverages battle-tested TanStack Pacer utilities
346+
-Reduces backend write contention
347+
-Framework-agnostic core promotes consistency
348+
-Type-safe, composable API
349+
-Aligns with TanStack ecosystem patterns
350+
-Supports all 5 framework integrations
351+
-Simple, declarative API for users
352+
-Easy to add custom strategies
353+
354+
## Open Questions
355+
356+
1. Should we support custom strategies? (i.e., users passing their own strategy objects)
357+
2. Do we need lifecycle callbacks like `onSuccess`, `onError` for each mutate call?
358+
3. Should batching strategy automatically merge mutations or keep them separate?
359+
4. Rate limiting strategy - useful or skip for now?
360+
361+
## Notes
362+
363+
-Dropped merge strategy for now (more complex to design, less clear use case)
364+
- The pattern follows existing TanStack patterns where core is framework-agnostic
365+
- Similar to how `useLiveQuery` wraps core query logic per framework

0 commit comments

Comments
 (0)