@inglorious/store 9.6.0 → 11.0.0
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/README.md +100 -33
- package/package.json +3 -3
- package/src/async.js +10 -1
- package/src/optimistic.js +195 -0
- package/src/store.js +19 -10
- package/types/optimistic.d.ts +25 -0
package/README.md
CHANGED
|
@@ -118,8 +118,8 @@ const types = {
|
|
|
118
118
|
|
|
119
119
|
// Define initial entities
|
|
120
120
|
const entities = {
|
|
121
|
-
work: { type: "
|
|
122
|
-
personal: { type: "
|
|
121
|
+
work: { type: "TodoList", todos: [] },
|
|
122
|
+
personal: { type: "TodoList", todos: [] },
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Create store
|
|
@@ -171,9 +171,9 @@ const types = {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const entities = {
|
|
174
|
-
workTodos: { type: "
|
|
175
|
-
personalTodos: { type: "
|
|
176
|
-
settings: { type: "
|
|
174
|
+
workTodos: { type: "TodoList", todos: [], priority: "high" },
|
|
175
|
+
personalTodos: { type: "TodoList", todos: [], priority: "low" },
|
|
176
|
+
settings: { type: "Settings", theme: "dark", language: "en" },
|
|
177
177
|
}
|
|
178
178
|
```
|
|
179
179
|
|
|
@@ -241,7 +241,7 @@ const types = {
|
|
|
241
241
|
|
|
242
242
|
// 2. Define initial entities
|
|
243
243
|
const entities = {
|
|
244
|
-
counter1: { type: "
|
|
244
|
+
counter1: { type: "Counter", value: 0 },
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// 3. Create the store
|
|
@@ -352,12 +352,12 @@ const types = {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
const entities = {
|
|
355
|
-
counter1: { type: "
|
|
356
|
-
counter2: { type: "
|
|
357
|
-
counter3: { type: "
|
|
355
|
+
counter1: { type: "Counter", value: 0 },
|
|
356
|
+
counter2: { type: "Counter", value: 0 },
|
|
357
|
+
counter3: { type: "Counter", value: 0 },
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
store.notify("add", { id: "counter4", type: "
|
|
360
|
+
store.notify("add", { id: "counter4", type: "Counter", value: 0 })
|
|
361
361
|
```
|
|
362
362
|
|
|
363
363
|
Inglorious Store has a few built-in events that you can use:
|
|
@@ -508,27 +508,39 @@ The `handleAsync` helper generates a set of event handlers representing the life
|
|
|
508
508
|
handleAsync(type, handlers, options?)
|
|
509
509
|
```
|
|
510
510
|
|
|
511
|
+
Think of the flow like a newspaper article:
|
|
512
|
+
|
|
513
|
+
- `start` writes the headline and sets the scene
|
|
514
|
+
- `run` gathers the reporting
|
|
515
|
+
- `success` publishes the story
|
|
516
|
+
- `error` prints the correction
|
|
517
|
+
- `finally` archives the notes
|
|
518
|
+
|
|
511
519
|
Example:
|
|
512
520
|
|
|
513
521
|
```ts
|
|
514
|
-
handleAsync
|
|
515
|
-
async run(payload) {
|
|
516
|
-
const res = await fetch("/api/todos")
|
|
517
|
-
return res.json()
|
|
518
|
-
},
|
|
522
|
+
import { handleAsync } from "@inglorious/store/async"
|
|
519
523
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
524
|
+
const todoList = {
|
|
525
|
+
...handleAsync("fetchTodos", {
|
|
526
|
+
async run(payload) {
|
|
527
|
+
const res = await fetch("/api/todos")
|
|
528
|
+
return res.json()
|
|
529
|
+
},
|
|
523
530
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
531
|
+
success(entity, todos) {
|
|
532
|
+
entity.todos = todos
|
|
533
|
+
},
|
|
527
534
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
535
|
+
error(entity, error) {
|
|
536
|
+
entity.error = error.message
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
finally(entity) {
|
|
540
|
+
entity.loading = false
|
|
541
|
+
},
|
|
542
|
+
}),
|
|
543
|
+
}
|
|
532
544
|
```
|
|
533
545
|
|
|
534
546
|
---
|
|
@@ -565,6 +577,61 @@ handleAsync("save", {
|
|
|
565
577
|
|
|
566
578
|
If omitted, no `Start` event is generated.
|
|
567
579
|
|
|
580
|
+
### `optimistic`
|
|
581
|
+
|
|
582
|
+
Wrap an async behavior to apply a shallow optimistic patch on `Start` and roll it back on `Error`.
|
|
583
|
+
|
|
584
|
+
This helper lives at `@inglorious/store/optimistic`, so only users who need it import it.
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
import { handleAsync } from "@inglorious/store/async"
|
|
588
|
+
import { optimistic } from "@inglorious/store/optimistic"
|
|
589
|
+
|
|
590
|
+
const saveItem = optimistic(
|
|
591
|
+
handleAsync("saveItem", {
|
|
592
|
+
async run(payload) {
|
|
593
|
+
const res = await fetch("/api/items", {
|
|
594
|
+
method: "POST",
|
|
595
|
+
body: JSON.stringify(payload),
|
|
596
|
+
})
|
|
597
|
+
if (!res.ok) throw new Error("Failed")
|
|
598
|
+
return res.json()
|
|
599
|
+
},
|
|
600
|
+
success(entity, result) {
|
|
601
|
+
entity.items = entity.items.map((item) =>
|
|
602
|
+
item.id === result.tempId ? result : item,
|
|
603
|
+
)
|
|
604
|
+
},
|
|
605
|
+
}),
|
|
606
|
+
(entity, payload) => ({
|
|
607
|
+
items: [
|
|
608
|
+
...entity.items,
|
|
609
|
+
{
|
|
610
|
+
id: payload.tempId,
|
|
611
|
+
title: payload.title,
|
|
612
|
+
completed: payload.completed,
|
|
613
|
+
pending: true,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
}),
|
|
617
|
+
)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
If the optimistic state does not depend on the payload, use a static patch instead:
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
const saveSettings = optimistic(
|
|
624
|
+
handleAsync("saveSettings", {
|
|
625
|
+
async run(payload) {
|
|
626
|
+
return api.save(payload)
|
|
627
|
+
},
|
|
628
|
+
}),
|
|
629
|
+
{ status: "saving" },
|
|
630
|
+
)
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
The wrapper snapshots the keys in the patch, applies them in `Start`, restores the previous values in `Error`, and leaves `Success`/`Finally` handlers in control of cleanup.
|
|
634
|
+
|
|
568
635
|
---
|
|
569
636
|
|
|
570
637
|
### Event scoping
|
|
@@ -707,7 +774,7 @@ function increment(entity, payload, api) {
|
|
|
707
774
|
|
|
708
775
|
// Test it
|
|
709
776
|
const { entity, events } = trigger(
|
|
710
|
-
{ type: "
|
|
777
|
+
{ type: "Counter", id: "counter1", value: 99 },
|
|
711
778
|
increment,
|
|
712
779
|
{ amount: 5 },
|
|
713
780
|
)
|
|
@@ -725,8 +792,8 @@ import { createMockApi, trigger } from "@inglorious/store/test"
|
|
|
725
792
|
|
|
726
793
|
// Create a mock API with some initial entities
|
|
727
794
|
const api = createMockApi({
|
|
728
|
-
counter1: { type: "
|
|
729
|
-
counter2: { type: "
|
|
795
|
+
counter1: { type: "Counter", value: 10 },
|
|
796
|
+
counter2: { type: "Counter", value: 20 },
|
|
730
797
|
})
|
|
731
798
|
|
|
732
799
|
// A handler that copies a value from another entity
|
|
@@ -737,7 +804,7 @@ function copyValue(entity, payload, api) {
|
|
|
737
804
|
|
|
738
805
|
// Trigger the handler with the custom mock API
|
|
739
806
|
const { entity } = trigger(
|
|
740
|
-
{ type: "
|
|
807
|
+
{ type: "Counter", id: "counter2", value: 20 },
|
|
741
808
|
copyValue,
|
|
742
809
|
{ sourceId: "counter1" },
|
|
743
810
|
api,
|
|
@@ -1042,15 +1109,15 @@ const types = {
|
|
|
1042
1109
|
|
|
1043
1110
|
// Without autoCreateEntities (default)
|
|
1044
1111
|
const entities = {
|
|
1045
|
-
settings: { type: "
|
|
1046
|
-
analytics: { type: "
|
|
1112
|
+
settings: { type: "Settings", theme: "dark" },
|
|
1113
|
+
analytics: { type: "Analytics", events: [] },
|
|
1047
1114
|
}
|
|
1048
1115
|
|
|
1049
1116
|
// With autoCreateEntities: true
|
|
1050
1117
|
const entities = {
|
|
1051
1118
|
// settings and analytics will be auto-created as:
|
|
1052
|
-
// settings: { type: "
|
|
1053
|
-
// analytics: { type: "
|
|
1119
|
+
// settings: { type: "Settings" }
|
|
1120
|
+
// analytics: { type: "Analytics" }
|
|
1054
1121
|
}
|
|
1055
1122
|
|
|
1056
1123
|
const store = createStore({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "A state manager for real-time, collaborative apps, inspired by game development patterns and compatible with Redux.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"mutative": "^1.3.0",
|
|
59
|
-
"@inglorious/utils": "3.
|
|
59
|
+
"@inglorious/utils": "3.8.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@inglorious/utils": "3.
|
|
62
|
+
"@inglorious/utils": "3.8.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@reduxjs/toolkit": "^2.11.2",
|
package/src/async.js
CHANGED
|
@@ -146,7 +146,7 @@ export function handleAsync(type, handlers, options = {}) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
const asyncHandlers = {
|
|
150
150
|
[type](entity, payload, api) {
|
|
151
151
|
if (handlers.start) {
|
|
152
152
|
notify(api, entity, `${type}Start`, payload)
|
|
@@ -184,4 +184,13 @@ export function handleAsync(type, handlers, options = {}) {
|
|
|
184
184
|
handlers.finally?.(entity, api)
|
|
185
185
|
},
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
Object.defineProperty(asyncHandlers, ASYNC_META, {
|
|
189
|
+
value: { type, scope },
|
|
190
|
+
enumerable: false,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return asyncHandlers
|
|
187
194
|
}
|
|
195
|
+
|
|
196
|
+
export const ASYNC_META = Symbol.for("@inglorious/store/asyncMeta")
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { ASYNC_META } from "./async.js"
|
|
2
|
+
|
|
3
|
+
const START = 0
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decorates an async behavior with optimistic update boilerplate.
|
|
7
|
+
*
|
|
8
|
+
* Pass either a static shallow patch object or a factory that returns one.
|
|
9
|
+
* The wrapper applies the patch during `Start` and rolls it back during `Error`.
|
|
10
|
+
*
|
|
11
|
+
* @template {import('./store').BaseEntity} TEntity
|
|
12
|
+
* @template TPayload
|
|
13
|
+
* @template TResult
|
|
14
|
+
*
|
|
15
|
+
* @param {import('./async').AsyncEventHandlers<TEntity, TPayload, TResult>} behavior
|
|
16
|
+
* @param {Record<string, any> | ((entity: TEntity, payload: TPayload, api: import('./store').Api) => Record<string, any> | null | undefined)} optimistic
|
|
17
|
+
* Shallow patch applied optimistically on start, or a factory that returns it.
|
|
18
|
+
*
|
|
19
|
+
* @returns {import('./async').AsyncEventHandlers<TEntity, TPayload, TResult>}
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const saveItem = optimistic(
|
|
23
|
+
* handleAsync("saveItem", {
|
|
24
|
+
* async run(payload) {
|
|
25
|
+
* return fetch("/api/items", {
|
|
26
|
+
* method: "POST",
|
|
27
|
+
* body: JSON.stringify(payload),
|
|
28
|
+
* }).then((res) => res.json())
|
|
29
|
+
* },
|
|
30
|
+
* success(entity, result) {
|
|
31
|
+
* entity.data = result
|
|
32
|
+
* },
|
|
33
|
+
* }),
|
|
34
|
+
* { status: "saving" },
|
|
35
|
+
* )
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const saveDraft = optimistic(
|
|
39
|
+
* handleAsync("saveDraft", {
|
|
40
|
+
* async run(payload) {
|
|
41
|
+
* return api.save(payload)
|
|
42
|
+
* },
|
|
43
|
+
* }),
|
|
44
|
+
* (entity, payload) => ({
|
|
45
|
+
* data: { ...entity.data, ...payload },
|
|
46
|
+
* }),
|
|
47
|
+
* )
|
|
48
|
+
*/
|
|
49
|
+
export function optimistic(behavior, optimistic) {
|
|
50
|
+
const { type, scope } = resolveAsyncMeta(behavior)
|
|
51
|
+
|
|
52
|
+
const startEvent = `${type}Start`
|
|
53
|
+
const runEvent = `${type}Run`
|
|
54
|
+
const errorEvent = `${type}Error`
|
|
55
|
+
const finallyEvent = `${type}Finally`
|
|
56
|
+
|
|
57
|
+
const snapshots = new Map()
|
|
58
|
+
|
|
59
|
+
function notify(api, entity, event, payload) {
|
|
60
|
+
switch (scope) {
|
|
61
|
+
case "entity":
|
|
62
|
+
api.notify(`#${entity.id}:${event}`, payload)
|
|
63
|
+
break
|
|
64
|
+
case "type":
|
|
65
|
+
api.notify(`${entity.type}:${event}`, payload)
|
|
66
|
+
break
|
|
67
|
+
case "global":
|
|
68
|
+
api.notify(event, payload)
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const wrapped = {
|
|
74
|
+
...behavior,
|
|
75
|
+
|
|
76
|
+
[type](entity, payload, api) {
|
|
77
|
+
notify(api, entity, startEvent, payload)
|
|
78
|
+
notify(api, entity, runEvent, payload)
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
[startEvent](entity, payload, api) {
|
|
82
|
+
const patch = toPatch(optimistic, entity, payload, api)
|
|
83
|
+
|
|
84
|
+
if (!patch) {
|
|
85
|
+
behavior[startEvent]?.(entity, payload, api)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof patch !== "object") {
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
"optimistic expects the patch to be an object or a factory that returns an object",
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const key = getEntityKey(entity)
|
|
96
|
+
const currentEntity = api.getEntity?.(key) ?? entity
|
|
97
|
+
snapshots.set(key, snapshotPatch(currentEntity, patch))
|
|
98
|
+
Object.assign(entity, patch)
|
|
99
|
+
|
|
100
|
+
behavior[startEvent]?.(entity, payload, api)
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
[runEvent]: behavior[runEvent],
|
|
104
|
+
|
|
105
|
+
[errorEvent](entity, error, api) {
|
|
106
|
+
const key = getEntityKey(entity)
|
|
107
|
+
restorePatch(entity, snapshots.get(key))
|
|
108
|
+
behavior[errorEvent]?.(entity, error, api)
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
[finallyEvent](entity, api) {
|
|
112
|
+
try {
|
|
113
|
+
behavior[finallyEvent]?.(entity, api)
|
|
114
|
+
} finally {
|
|
115
|
+
snapshots.delete(getEntityKey(entity))
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Object.defineProperty(wrapped, ASYNC_META, {
|
|
121
|
+
value: { type, scope },
|
|
122
|
+
enumerable: false,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return wrapped
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveAsyncMeta(behavior) {
|
|
129
|
+
const meta = behavior?.[ASYNC_META]
|
|
130
|
+
if (meta?.type) {
|
|
131
|
+
return meta
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const runHandlers = Object.keys(behavior ?? {}).filter(
|
|
135
|
+
(key) => key.endsWith("Run") && typeof behavior[key] === "function",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const [runHandler, ...rest] = runHandlers
|
|
139
|
+
|
|
140
|
+
if (!runHandler && rest.length) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"optimistic expects a single handleAsync result or one '*Run' handler",
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: runHandler.slice(START, -"Run".length),
|
|
148
|
+
scope: "entity",
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getEntityKey(entity) {
|
|
153
|
+
const id = entity?.id
|
|
154
|
+
if (id == null) {
|
|
155
|
+
throw new Error("optimistic requires entities to have a stable `id`")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return id
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toPatch(optimistic, entity, payload, api) {
|
|
162
|
+
if (typeof optimistic === "function") {
|
|
163
|
+
return optimistic(entity, payload, api)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return optimistic
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function snapshotPatch(entity, patch) {
|
|
170
|
+
const entries = []
|
|
171
|
+
|
|
172
|
+
for (const key of Object.keys(patch)) {
|
|
173
|
+
entries.push([
|
|
174
|
+
key,
|
|
175
|
+
Object.prototype.hasOwnProperty.call(entity, key),
|
|
176
|
+
entity[key],
|
|
177
|
+
])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return entries
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function restorePatch(entity, entries) {
|
|
184
|
+
if (!entries) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const [key, existed, value] of entries) {
|
|
189
|
+
if (existed) {
|
|
190
|
+
entity[key] = value
|
|
191
|
+
} else {
|
|
192
|
+
delete entity[key]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toCamelCase } from "@inglorious/utils/data-structures/string.js"
|
|
1
2
|
import { create } from "mutative"
|
|
2
3
|
|
|
3
4
|
import { createApi } from "./api.js"
|
|
@@ -118,8 +119,19 @@ export function createStore({
|
|
|
118
119
|
const type = types[entity.type]
|
|
119
120
|
const handle = type[handlerName]
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
handle?.(entity, event.payload, api)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (event.type.endsWith(":destroy")) {
|
|
126
|
+
const [target] = event.type.split(":")
|
|
127
|
+
const [, id] = target.split("#")
|
|
128
|
+
const entity = draft[id]
|
|
129
|
+
if (entity) {
|
|
130
|
+
const type = types[entity.type]
|
|
131
|
+
const typeName = entity.type
|
|
132
|
+
delete draft[id]
|
|
133
|
+
|
|
134
|
+
eventMap.removeEntity(id, type, typeName)
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -197,9 +209,10 @@ export function createStore({
|
|
|
197
209
|
}
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
const entityId = toCamelCase(typeName)
|
|
213
|
+
if (autoCreateEntities && !state[entityId]) {
|
|
201
214
|
notify("add", {
|
|
202
|
-
id:
|
|
215
|
+
id: entityId,
|
|
203
216
|
type: typeName,
|
|
204
217
|
})
|
|
205
218
|
}
|
|
@@ -248,12 +261,13 @@ export function createStore({
|
|
|
248
261
|
|
|
249
262
|
if (autoCreateEntities) {
|
|
250
263
|
for (const typeName of Object.keys(types)) {
|
|
264
|
+
const entityId = toCamelCase(typeName)
|
|
251
265
|
const hasEntity = Object.values(state).some(
|
|
252
266
|
(entity) => entity.type === typeName,
|
|
253
267
|
)
|
|
254
268
|
|
|
255
269
|
if (!hasEntity) {
|
|
256
|
-
addEntity(state, { id:
|
|
270
|
+
addEntity(state, { id: entityId, type: typeName })
|
|
257
271
|
}
|
|
258
272
|
}
|
|
259
273
|
}
|
|
@@ -277,12 +291,7 @@ export function createStore({
|
|
|
277
291
|
|
|
278
292
|
function removeEntity(draft, payload) {
|
|
279
293
|
const id = payload
|
|
280
|
-
const entity = draft[id]
|
|
281
|
-
const type = types[entity.type]
|
|
282
|
-
const typeName = entity.type
|
|
283
|
-
delete draft[id]
|
|
284
294
|
|
|
285
|
-
eventMap.removeEntity(id, type, typeName)
|
|
286
295
|
incomingEvents.unshift({ type: `#${id}:destroy` })
|
|
287
296
|
}
|
|
288
297
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Api } from "./api"
|
|
2
|
+
import { AsyncEventHandlers } from "./async"
|
|
3
|
+
import { BaseEntity } from "./store"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decorates an async behavior with optimistic update boilerplate.
|
|
7
|
+
*
|
|
8
|
+
* Pass either a static shallow patch object or a factory that returns one.
|
|
9
|
+
* The wrapper snapshots the current values for those keys, applies the patch
|
|
10
|
+
* on start, and restores the previous values automatically if the operation fails.
|
|
11
|
+
*/
|
|
12
|
+
export function optimistic<
|
|
13
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
14
|
+
TPayload = any,
|
|
15
|
+
TResult = any,
|
|
16
|
+
>(
|
|
17
|
+
behavior: AsyncEventHandlers<TEntity, TPayload, TResult>,
|
|
18
|
+
optimistic:
|
|
19
|
+
| Record<string, any>
|
|
20
|
+
| ((
|
|
21
|
+
entity: TEntity,
|
|
22
|
+
payload: TPayload,
|
|
23
|
+
api: Api,
|
|
24
|
+
) => Record<string, any> | null | undefined),
|
|
25
|
+
): AsyncEventHandlers<TEntity, TPayload, TResult>
|