Skip to content

Commit 2f6c67c

Browse files
committed
chore: lint/format
1 parent 5d662d0 commit 2f6c67c

6 files changed

Lines changed: 165 additions & 111 deletions

File tree

README.md

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Most optimistic-update libraries snapshot your entire state tree for every in-flight operation. Optimistron doesn't. It tracks lightweight **transitions** (stage, amend, commit, fail) alongside your reducer state and replays them at read-time through `selectOptimistic` — right where `reselect` memoization already lives.
1717

1818
Good fit for:
19+
1920
- **Offline-first** — transitions queue up while disconnected, conflicts resolve on reconnect
2021
- **Async dispatch** — thunks, sagas, listener middleware
2122
- **Large/normalized state** — no per-operation snapshots
@@ -28,16 +29,16 @@ Good fit for:
2829

2930
Think of each `optimistron()` reducer as a **git branch**:
3031

31-
| Git | Optimistron |
32-
|-----|-------------|
33-
| Branch tip | **Committed state** — only `COMMIT` advances it |
34-
| Staged commits | **Transitions** — pending changes on top of committed state |
35-
| `git rebase` | **`selectOptimistic`** — replays transitions at read-time |
32+
| Git | Optimistron |
33+
| -------------- | -------------------------------------------------------------------- |
34+
| Branch tip | **Committed state** — only `COMMIT` advances it |
35+
| Staged commits | **Transitions** — pending changes on top of committed state |
36+
| `git rebase` | **`selectOptimistic`** — replays transitions at read-time |
3637
| Merge conflict | **Sanitization** — detects no-ops and conflicts after every mutation |
3738

3839
`STAGE`, `AMEND`, `FAIL`, `STASH` never touch reducer state — they only modify the transitions list. The optimistic view updates because `selectOptimistic` re-derives on the next read.
3940

40-
No `isLoading` / `error` / `isOptimistic` flags. A pending transition *is* loading. A failed one *is* the error. One source of truth.
41+
No `isLoading` / `error` / `isOptimistic` flags. A pending transition _is_ loading. A failed one _is_ the error. One source of truth.
4142

4243
---
4344

@@ -53,7 +54,7 @@ No `isLoading` / `error` / `isOptimistic` flags. A pending transition *is* loadi
5354

5455
```bash
5556
npm install @lostsolution/optimistron
56-
# peer deps: @reduxjs/toolkit ^2.1.0, redux ^5.0.1
57+
# ⚠️ not published yet
5758
```
5859

5960
---
@@ -62,10 +63,7 @@ npm install @lostsolution/optimistron
6263

6364
```typescript
6465
import { configureStore, createSelector } from '@reduxjs/toolkit';
65-
import {
66-
optimistron, createTransitions, crudPrepare,
67-
recordState, TransitionMode,
68-
} from '@lostsolution/optimistron';
66+
import { optimistron, createTransitions, crudPrepare, recordState, TransitionMode } from '@lostsolution/optimistron';
6967

7068
// 1. Define your entity
7169
type Todo = { id: string; value: string; done: boolean; revision: number };
@@ -75,33 +73,33 @@ const crud = crudPrepare<Todo>('id');
7573

7674
// 3. Create transition action creators
7775
const createTodo = createTransitions('todos::add', TransitionMode.DISPOSABLE)(crud.create);
78-
const editTodo = createTransitions('todos::edit')(crud.update); // DEFAULT mode
76+
const editTodo = createTransitions('todos::edit')(crud.update); // DEFAULT mode
7977
const deleteTodo = createTransitions('todos::delete', TransitionMode.REVERTIBLE)(crud.remove);
8078

8179
// 4. Create the optimistic reducer
8280
const { reducer: todos, selectOptimistic } = optimistron(
83-
'todos',
84-
{} as Record<string, Todo>,
85-
recordState<Todo>({
86-
key: 'id',
87-
compare: (a) => (b) => (a.revision === b.revision ? 0 : a.revision > b.revision ? 1 : -1),
88-
eq: (a) => (b) => a.done === b.done && a.value === b.value,
89-
}),
90-
{ create: createTodo, update: editTodo, remove: deleteTodo },
81+
'todos',
82+
{} as Record<string, Todo>,
83+
recordState<Todo>({
84+
key: 'id',
85+
compare: (a) => (b) => (a.revision === b.revision ? 0 : a.revision > b.revision ? 1 : -1),
86+
eq: (a) => (b) => a.done === b.done && a.value === b.value,
87+
}),
88+
{ create: createTodo, update: editTodo, remove: deleteTodo },
9189
);
9290

9391
// 5. Wire up the store
9492
const store = configureStore({ reducer: { todos } });
9593

9694
// 6. Select optimistic state (memoize with createSelector)
9795
const selectTodos = createSelector(
98-
(state: RootState) => state.todos,
99-
selectOptimistic((todos) => Object.values(todos.state)),
96+
(state: RootState) => state.todos,
97+
selectOptimistic((todos) => Object.values(todos.state)),
10098
);
10199

102100
// 7. Dispatch transitions
103-
dispatch(createTodo.stage(todo)); // optimistic — shows immediately
104-
dispatch(createTodo.commit(todo.id)); // server confirmed — becomes committed state
101+
dispatch(createTodo.stage(todo)); // optimistic — shows immediately
102+
dispatch(createTodo.commit(todo.id)); // server confirmed — becomes committed state
105103
dispatch(createTodo.fail(todo.id, error)); // server rejected — flagged as failed
106104
```
107105

@@ -120,8 +118,8 @@ dispatch(createTodo.fail(todo.id, error)); // server rejected — flagged as fai
120118
Entities need a **monotonically increasing version**`revision`, `updatedAt`, a sequence number. This is how sanitization tells "newer" from "stale":
121119

122120
```typescript
123-
compare: (a) => (b) => 0 | 1 | -1 // version ordering (curried)
124-
eq: (a) => (b) => boolean // content equality at same version (curried)
121+
compare: (a) => (b) => 0 | 1 | -1; // version ordering (curried)
122+
eq: (a) => (b) => boolean; // content equality at same version (curried)
125123
```
126124

127125
Without versioning, conflict detection degrades to content equality only.
@@ -176,27 +174,36 @@ You can implement the `StateHandler` interface for any shape — the built-ins a
176174
The 4th argument to `optimistron()` supports three modes:
177175

178176
**Auto-wired** — zero boilerplate, handler routes payloads:
177+
179178
```typescript
180179
optimistron('todos', initial, handler, {
181-
create: createTodo, update: editTodo, remove: deleteTodo,
180+
create: createTodo,
181+
update: editTodo,
182+
remove: deleteTodo,
182183
});
183184
```
184185

185186
**Hybrid** — auto-wire + fallback for custom actions:
187+
186188
```typescript
187189
optimistron('todos', initial, handler, {
188-
create: createTodo, update: editTodo, remove: deleteTodo,
189-
reducer: ({ getState }, action) => { /* custom logic */ },
190+
create: createTodo,
191+
update: editTodo,
192+
remove: deleteTodo,
193+
reducer: ({ getState }, action) => {
194+
/* custom logic */
195+
},
190196
});
191197
```
192198

193199
**Manual** — full control via `BoundStateHandler`:
200+
194201
```typescript
195202
optimistron('todos', initial, handler, ({ getState, create, update, remove }, action) => {
196-
if (createTodo.match(action)) return create(action.payload);
197-
if (editTodo.match(action)) return update(action.payload);
198-
if (deleteTodo.match(action)) return remove(action.payload);
199-
return getState();
203+
if (createTodo.match(action)) return create(action.payload);
204+
if (editTodo.match(action)) return update(action.payload);
205+
if (deleteTodo.match(action)) return remove(action.payload);
206+
return getState();
200207
});
201208
```
202209

@@ -206,11 +213,11 @@ optimistron('todos', initial, handler, ({ getState, create, update, remove }, ac
206213

207214
Declared per action type — controls what happens on re-stage and failure:
208215

209-
| Mode | On re-stage | On fail | Typical use |
210-
|------|-------------|---------|-------------|
211-
| `DEFAULT` | Overwrite | Flag as failed | Edits |
212-
| `DISPOSABLE` | Overwrite | Drop transition | Creates |
213-
| `REVERTIBLE` | Store trailing | Revert to previous | Deletes |
216+
| Mode | On re-stage | On fail | Typical use |
217+
| ------------ | -------------- | ------------------ | ----------- |
218+
| `DEFAULT` | Overwrite | Flag as failed | Edits |
219+
| `DISPOSABLE` | Overwrite | Drop transition | Creates |
220+
| `REVERTIBLE` | Store trailing | Revert to previous | Deletes |
214221

215222
---
216223

@@ -220,8 +227,8 @@ Declared per action type — controls what happens on re-stage and failure:
220227

221228
```typescript
222229
const selectTodos = createSelector(
223-
(state: RootState) => state.todos,
224-
selectOptimistic((todos) => Object.values(todos.state)),
230+
(state: RootState) => state.todos,
231+
selectOptimistic((todos) => Object.values(todos.state)),
225232
);
226233
```
227234

@@ -230,9 +237,9 @@ const selectTodos = createSelector(
230237
```typescript
231238
import { selectIsOptimistic, selectIsFailed, selectIsConflicting } from '@lostsolution/optimistron';
232239

233-
selectIsOptimistic(id)(state.todos) // pending?
234-
selectIsFailed(id)(state.todos) // failed?
235-
selectIsConflicting(id)(state.todos) // stale conflict?
240+
selectIsOptimistic(id)(state.todos); // pending?
241+
selectIsFailed(id)(state.todos); // failed?
242+
selectIsConflicting(id)(state.todos); // stale conflict?
236243
```
237244

238245
### Retry
@@ -242,7 +249,7 @@ import { retryTransition, selectFailedTransition, selectRetryCount } from '@lost
242249

243250
const failed = selectFailedTransition(id)(state.todos);
244251
if (failed && selectRetryCount(id)(state.todos) < 3) {
245-
dispatch(retryTransition(failed));
252+
dispatch(retryTransition(failed));
246253
}
247254
```
248255

src/reducer.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,10 @@ import type { BoundStateHandler, CrudActionMap, StateHandler, TransitionState, W
33

44
export type BoundReducer<State = any> = (state: TransitionState<State>, action: Action) => State;
55

6-
export type HandlerReducer<State, C = any, U = any, D = any> = (
7-
boundStateHandler: BoundStateHandler<State, C, U, D>,
8-
action: Action,
9-
) => State;
6+
export type HandlerReducer<State, C = any, U = any, D = any> = (boundStateHandler: BoundStateHandler<State, C, U, D>, action: Action) => State;
107

118
/** Consumer-facing reducer config: either a function (manual) or a CRUD map (auto-wired) */
12-
export type ReducerConfig<S, C = any, U = any, D = any> =
13-
| HandlerReducer<S, C, U, D>
14-
| (CrudActionMap & { reducer?: HandlerReducer<S, C, U, D> });
9+
export type ReducerConfig<S, C = any, U = any, D = any> = HandlerReducer<S, C, U, D> | (CrudActionMap & { reducer?: HandlerReducer<S, C, U, D> });
1510

1611
/** Runtime shape of the CRUD config branch — matches ActionMatcher's runtime interface.
1712
* Type-level safety is enforced at `optimistron()` call sites via overloads. */
@@ -27,10 +22,7 @@ type CrudConfigRuntime<S, C, U, D> = {
2722
type WiredHandler<S, C, U, D> = WiredStateHandler<S, C, U, D, CrudConfigRuntime<S, C, U, D>>;
2823

2924
/** Resolves a `ReducerConfig` to a `HandlerReducer` — auto-wires CRUD maps via the handler's `wire` method */
30-
export const resolveReducer = <S, C, U, D>(
31-
handler: StateHandler<S, C, U, D>,
32-
config: ReducerConfig<S, C, U, D>,
33-
): HandlerReducer<S, C, U, D> => {
25+
export const resolveReducer = <S, C, U, D>(handler: StateHandler<S, C, U, D>, config: ReducerConfig<S, C, U, D>): HandlerReducer<S, C, U, D> => {
3426
if (typeof config === 'function') return config;
3527

3628
if (!('wire' in handler) || typeof handler.wire !== 'function') {
@@ -49,9 +41,6 @@ export const resolveReducer = <S, C, U, D>(
4941
};
5042

5143
export const bindReducer =
52-
<S, C, U, D>(
53-
reducer: HandlerReducer<S, C, U, D>,
54-
bindState: (state: S) => BoundStateHandler<S, C, U, D>,
55-
): BoundReducer<S> =>
44+
<S, C, U, D>(reducer: HandlerReducer<S, C, U, D>, bindState: (state: S) => BoundStateHandler<S, C, U, D>): BoundReducer<S> =>
5645
(transitionState, action) =>
5746
reducer(bindState(transitionState.state), action);

src/transitions.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,7 @@ export const processTransition = (transition: TransitionAction, transitions: Sta
170170
* and performing a sanity 'merge' check on each iteration. This process helps cleanse the transitions
171171
* list by eliminating no-op actions and identifying potential conflicts. */
172172
export const sanitizeTransitions =
173-
<State, C, U, D>(
174-
boundReducer: BoundReducer<State>,
175-
bindState: ReturnType<typeof bindStateFactory<State, C, U, D>>,
176-
) =>
173+
<State, C, U, D>(boundReducer: BoundReducer<State>, bindState: ReturnType<typeof bindStateFactory<State, C, U, D>>) =>
177174
(state: TransitionState<State>) => {
178175
const sanitized = state.transitions.reduce<{
179176
mutated: boolean;

test/integration/nested.spec.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ const remove = createTransitions('nested::remove')(crud.remove);
3232

3333
type Keys = ['groupId', 'itemId'];
3434

35-
const reducer: HandlerReducer<State, Item, UpdateDTO<Item, Keys>, DeleteDTO<Item, Keys>> = (
36-
{ getState, create, update, remove: r },
37-
action,
38-
) => {
35+
const reducer: HandlerReducer<State, Item, UpdateDTO<Item, Keys>, DeleteDTO<Item, Keys>> = ({ getState, create, update, remove: r }, action) => {
3936
if (add.match(action)) return create(action.payload);
4037
if (edit.match(action)) return update(action.payload);
4138
if (remove.match(action)) return r(action.payload);

test/unit/optimistron.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, mock, test } from 'bun:test';
22

33
import { createTransitions } from '~actions';
44
import { optimistron } from '~optimistron';
5-
import { create, createItem, indexedState, reducer, remove, type TestItem } from '~test/utils';
5+
import { create, createItem, indexedState, reducer, type TestItem } from '~test/utils';
66
import { TransitionMode, getTransitionMeta, toCommit } from '~transitions';
77

88
describe('optimistron', () => {
@@ -64,7 +64,10 @@ describe('optimistron', () => {
6464
});
6565

6666
describe('TransitionMode.DISPOSABLE', () => {
67-
const disposableCreate = createTransitions('test::add', TransitionMode.DISPOSABLE)((item: TestItem) => ({
67+
const disposableCreate = createTransitions(
68+
'test::add',
69+
TransitionMode.DISPOSABLE,
70+
)((item: TestItem) => ({
6871
payload: item,
6972
transitionId: item.id,
7073
}));
@@ -101,7 +104,10 @@ describe('optimistron', () => {
101104
});
102105

103106
describe('TransitionMode.REVERTIBLE', () => {
104-
const revertibleDelete = createTransitions('test::remove', TransitionMode.REVERTIBLE)((dto: Pick<TestItem, 'id'>) => ({
107+
const revertibleDelete = createTransitions(
108+
'test::remove',
109+
TransitionMode.REVERTIBLE,
110+
)((dto: Pick<TestItem, 'id'>) => ({
105111
payload: dto,
106112
transitionId: dto.id,
107113
}));

0 commit comments

Comments
 (0)