@inglorious/store 11.0.1 → 11.0.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/README.md +74 -0
- package/package.json +7 -7
- package/src/client/devtools.js +21 -21
- package/src/client/multiplayer-middleware.js +1 -5
- package/src/entities.js +1 -1
- package/src/middlewares.js +1 -1
- package/src/migration/rtk.js +1 -1
- package/src/store.js +115 -17
- package/src/types.js +2 -3
- package/types/store.d.ts +24 -1
package/README.md
CHANGED
|
@@ -1072,6 +1072,7 @@ const store = createStore({
|
|
|
1072
1072
|
systems, // Array (optional): global state handlers
|
|
1073
1073
|
autoCreateEntities, // Boolean (optional): false (default) or true
|
|
1074
1074
|
updateMode, // String (optional): 'auto' (default) or 'manual'
|
|
1075
|
+
updateStrategy, // String (optional): 'structural-sharing' (default) or 'full-clone'
|
|
1075
1076
|
})
|
|
1076
1077
|
```
|
|
1077
1078
|
|
|
@@ -1088,6 +1089,79 @@ const store = createStore({
|
|
|
1088
1089
|
- **`updateMode`** (optional) - Controls when React components re-render:
|
|
1089
1090
|
- `'auto'` (default) - Automatic updates after each event
|
|
1090
1091
|
- `'manual'` - Manual control via `api.update()`
|
|
1092
|
+
- **`updateStrategy`** (optional) - Controls how the state is copied before queued events are applied:
|
|
1093
|
+
- `'structural-sharing'` (default) - Only the entities that actually changed are copied
|
|
1094
|
+
- `'full-clone'` - The whole state is deep-cloned and updated without a proxy
|
|
1095
|
+
|
|
1096
|
+
#### Update Strategy
|
|
1097
|
+
|
|
1098
|
+
`updateStrategy` decides how much copying work each update does. It exists because the right
|
|
1099
|
+
answer depends on how many entities change per update.
|
|
1100
|
+
|
|
1101
|
+
**`'structural-sharing'`** (default) applies events to a draft proxy (via
|
|
1102
|
+
[Mutative](https://github.com/pmndrs/mutative)), so only the entities that actually changed
|
|
1103
|
+
get copied. Untouched entities keep their previous object reference, which makes
|
|
1104
|
+
reference-equality change detection nearly free. This is what you want for UI stores, where a
|
|
1105
|
+
handful of entities change per interaction.
|
|
1106
|
+
|
|
1107
|
+
**`'full-clone'`** deep-clones the entire state with `structuredClone` and applies events directly to
|
|
1108
|
+
the copy, with no proxy involved. Every update costs time proportional to the total state size,
|
|
1109
|
+
but there's no per-entity proxy overhead, so it scales much better when thousands of entities
|
|
1110
|
+
change every frame — the typical case for a game simulation.
|
|
1111
|
+
|
|
1112
|
+
```javascript
|
|
1113
|
+
// A UI store: few entities change, reference equality matters
|
|
1114
|
+
const store = createStore({
|
|
1115
|
+
types,
|
|
1116
|
+
entities,
|
|
1117
|
+
updateStrategy: "structural-sharing",
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
// A game simulation: thousands of entities change every frame
|
|
1121
|
+
const store = createStore({ types, entities, updateStrategy: "full-clone" })
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
#### State swapping
|
|
1125
|
+
|
|
1126
|
+
Both strategies keep the current state intact while the queued events are applied to a separate
|
|
1127
|
+
draft, and swap the two only once every event has been processed. Handlers and systems always
|
|
1128
|
+
receive that draft as their first argument and mutate it directly, and subscribers are notified
|
|
1129
|
+
only after the swap.
|
|
1130
|
+
|
|
1131
|
+
A consequence worth knowing: during an update, `api.getEntity()` and `api.getEntities()` read the
|
|
1132
|
+
**previous** state, not the in-flight changes. This holds under both strategies, so code that
|
|
1133
|
+
needs to read another entity's up-to-date value must go through the event payload, or read it in
|
|
1134
|
+
a system that runs after the entity handlers.
|
|
1135
|
+
|
|
1136
|
+
```javascript
|
|
1137
|
+
const types = {
|
|
1138
|
+
Counter: {
|
|
1139
|
+
// Reads the previous value of `other`, not the value set earlier in this same update.
|
|
1140
|
+
increment(entity, payload, api) {
|
|
1141
|
+
entity.previous = api.getEntity("other").count
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
}
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
**Caveats for `'full-clone'`:**
|
|
1148
|
+
|
|
1149
|
+
- Every value in the state must be [structured-cloneable](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
|
|
1150
|
+
Functions, DOM nodes, and class instances with non-cloneable internals throw a
|
|
1151
|
+
`DataCloneError`. Keep such values in module scope or in a closure inside a behavior factory
|
|
1152
|
+
(as the `images()` and `audio()` engine behaviors do) rather than in the state.
|
|
1153
|
+
- The state is not frozen, even in `devMode`.
|
|
1154
|
+
- Writing to an entity obtained from `api.getEntity()` during an update mutates the _previous_
|
|
1155
|
+
state and is discarded, because the draft is a deep copy. Under `'structural-sharing'` the
|
|
1156
|
+
draft may still share that reference, so the write happens to survive. Always mutate the
|
|
1157
|
+
entity passed to your handler rather than one fetched through the api.
|
|
1158
|
+
|
|
1159
|
+
An unsupported strategy throws a `TypeError` at store creation:
|
|
1160
|
+
|
|
1161
|
+
```javascript
|
|
1162
|
+
createStore({ types, entities, updateStrategy: "immutable" })
|
|
1163
|
+
// TypeError: Unsupported update strategy: immutable. Expected one of: structural-sharing, full-clone.
|
|
1164
|
+
```
|
|
1091
1165
|
|
|
1092
1166
|
#### Auto-Create Entities
|
|
1093
1167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "11.0.
|
|
3
|
+
"version": "11.0.2",
|
|
4
4
|
"description": "A state manager for real-time, collaborative apps, inspired by game development patterns and compatible with Redux.",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,18 +55,18 @@
|
|
|
55
55
|
},
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@inglorious/utils": "
|
|
58
|
+
"@inglorious/utils": "4.0.0",
|
|
59
59
|
"mutative": "^1.3.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@inglorious/utils": "
|
|
62
|
+
"@inglorious/utils": "4.0.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@inglorious/eslint-config": "1.1.2",
|
|
66
|
-
"@reduxjs/toolkit": "^2.
|
|
67
|
-
"prettier": "^3.6
|
|
68
|
-
"vite": "^
|
|
69
|
-
"vitest": "^
|
|
66
|
+
"@reduxjs/toolkit": "^2.12.0",
|
|
67
|
+
"prettier": "^3.9.6",
|
|
68
|
+
"vite": "^8.2.2",
|
|
69
|
+
"vitest": "^5.0.0"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">= 22"
|
package/src/client/devtools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deserialize } from "@inglorious/utils/
|
|
1
|
+
import { deserialize } from "@inglorious/utils/objects"
|
|
2
2
|
|
|
3
3
|
const LAST_STATE = 1
|
|
4
4
|
|
|
@@ -55,21 +55,21 @@ function connectDevTools(store, config = {}) {
|
|
|
55
55
|
// Hot-swap store without resetting DevTools history
|
|
56
56
|
existing.restoreSetState?.()
|
|
57
57
|
const baseSetState = store.setState
|
|
58
|
-
|
|
58
|
+
const restoreSetState =
|
|
59
|
+
updateMode === "auto"
|
|
60
|
+
? () => {
|
|
61
|
+
if (store.setState !== baseSetState) {
|
|
62
|
+
store.setState = baseSetState
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
: () => {
|
|
66
|
+
// no-op when not wrapping setState
|
|
67
|
+
}
|
|
59
68
|
if (updateMode === "auto") {
|
|
60
|
-
restoreSetState = () => {
|
|
61
|
-
if (store.setState !== baseSetState) {
|
|
62
|
-
store.setState = baseSetState
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
69
|
store.setState = (newState) => {
|
|
66
70
|
baseSetState(newState)
|
|
67
71
|
sendAction({ type: "stateInit", payload: newState }, store.getState())
|
|
68
72
|
}
|
|
69
|
-
} else {
|
|
70
|
-
restoreSetState = () => {
|
|
71
|
-
// no-op when not wrapping setState
|
|
72
|
-
}
|
|
73
73
|
}
|
|
74
74
|
existing.store = store
|
|
75
75
|
existing.restoreSetState = restoreSetState
|
|
@@ -90,22 +90,22 @@ function connectDevTools(store, config = {}) {
|
|
|
90
90
|
|
|
91
91
|
const name = config.name ?? document.title
|
|
92
92
|
const baseSetState = store.setState
|
|
93
|
-
|
|
93
|
+
const restoreSetState =
|
|
94
|
+
updateMode === "auto"
|
|
95
|
+
? () => {
|
|
96
|
+
if (store.setState !== baseSetState) {
|
|
97
|
+
store.setState = baseSetState
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
: () => {
|
|
101
|
+
// noop; we didn't wrap setState
|
|
102
|
+
}
|
|
94
103
|
// Only add setState side-effects in auto update mode; manual update mode logs explicitly from engine.
|
|
95
104
|
if (updateMode === "auto") {
|
|
96
|
-
restoreSetState = () => {
|
|
97
|
-
if (store.setState !== baseSetState) {
|
|
98
|
-
store.setState = baseSetState
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
105
|
store.setState = (newState) => {
|
|
102
106
|
baseSetState(newState)
|
|
103
107
|
sendAction({ type: "stateInit", payload: newState }, store.getState())
|
|
104
108
|
}
|
|
105
|
-
} else {
|
|
106
|
-
restoreSetState = () => {
|
|
107
|
-
// noop; we didn't wrap setState
|
|
108
|
-
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const devToolsInstance = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deserialize,
|
|
3
|
-
serialize,
|
|
4
|
-
} from "@inglorious/utils/data-structures/object.js"
|
|
5
|
-
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
1
|
+
import { deserialize, extend, serialize } from "@inglorious/utils/objects"
|
|
6
2
|
|
|
7
3
|
// A constant for the server's WebSocket URL.
|
|
8
4
|
const DEFAULT_SERVER_URL = `ws://${window.location.hostname}:3000`
|
package/src/entities.js
CHANGED
package/src/middlewares.js
CHANGED
package/src/migration/rtk.js
CHANGED
package/src/store.js
CHANGED
|
@@ -7,16 +7,46 @@ import { EventMap, parseEvent } from "./event-map.js"
|
|
|
7
7
|
import { applyMiddlewares } from "./middlewares.js"
|
|
8
8
|
import { augmentType, augmentTypes } from "./types.js"
|
|
9
9
|
|
|
10
|
+
/** Copies only the entities that change, using a draft proxy. */
|
|
11
|
+
const STRUCTURAL_SHARING = "structural-sharing"
|
|
12
|
+
|
|
13
|
+
/** Deep-clones the whole state and applies events to the copy, without a proxy. */
|
|
14
|
+
const FULL_CLONE = "full-clone"
|
|
15
|
+
|
|
16
|
+
const UPDATE_STRATEGIES = [STRUCTURAL_SHARING, FULL_CLONE]
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Creates a store to manage state and events.
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Events are queued and drained by {@link createStore|update}. The `updateStrategy` option
|
|
22
|
+
* controls how the state is copied before the queued events are applied:
|
|
23
|
+
*
|
|
24
|
+
* - `"structural-sharing"` (default) - Applies events to a draft proxy, so only the entities
|
|
25
|
+
* that actually changed are copied. Unchanged entities keep their previous reference, which
|
|
26
|
+
* makes change detection cheap for consumers that rely on reference equality. This is the
|
|
27
|
+
* right choice for UI stores, where a small number of entities change at a time.
|
|
28
|
+
* - `"full-clone"` - Deep-clones the whole state with `structuredClone` and applies events directly
|
|
29
|
+
* to the copy, without a proxy. The cost is proportional to the total state size on every
|
|
30
|
+
* update, but there is no per-entity proxy overhead, so it scales better when thousands of
|
|
31
|
+
* entities change every frame, as in a game simulation.
|
|
32
|
+
*
|
|
33
|
+
* Under both strategies the current state is kept intact while the queued events are applied to a
|
|
34
|
+
* separate draft, and the two are swapped only once every event has been processed. Handlers and
|
|
35
|
+
* systems therefore always receive the draft as their first argument and mutate it directly, and
|
|
36
|
+
* subscribers are notified only after the swap. This also means that, during an update,
|
|
37
|
+
* `api.getEntity()` and `api.getEntities()` read the *previous* state rather than the in-flight
|
|
38
|
+
* changes, under either strategy.
|
|
39
|
+
*
|
|
40
|
+
* @param {StoreConfig} config - Configuration options for the store.
|
|
41
|
+
* @param {Types} [config.types] - The initial types configuration.
|
|
42
|
+
* @param {Entities} [config.entities] - The initial entities configuration.
|
|
43
|
+
* @param {Object[]} [config.systems] - The initial systems configuration.
|
|
44
|
+
* @param {Function[]} [config.middlewares] - The initial middlewares configuration.
|
|
45
|
+
* @param {boolean} [config.autoCreateEntities=false] - Creates entities if not defined in `config.entities`.
|
|
46
|
+
* @param {"auto" | "manual"} [config.updateMode="auto"] - Whether each event triggers an update, or updates are batched until `update()` is called.
|
|
47
|
+
* @param {UpdateStrategy} [config.updateStrategy="structural-sharing"] - How the state is copied before events are applied.
|
|
48
|
+
* @returns {Store} The store with methods to interact with state and events.
|
|
49
|
+
* @throws {TypeError} If `updateStrategy` is not a supported strategy.
|
|
20
50
|
*/
|
|
21
51
|
export function createStore({
|
|
22
52
|
types: originalTypes = {},
|
|
@@ -25,7 +55,10 @@ export function createStore({
|
|
|
25
55
|
middlewares = [],
|
|
26
56
|
autoCreateEntities = false,
|
|
27
57
|
updateMode = "auto",
|
|
58
|
+
updateStrategy = STRUCTURAL_SHARING,
|
|
28
59
|
} = {}) {
|
|
60
|
+
assertUpdateStrategy(updateStrategy)
|
|
61
|
+
|
|
29
62
|
const listeners = new Set()
|
|
30
63
|
|
|
31
64
|
const types = augmentTypes(originalTypes)
|
|
@@ -73,6 +106,12 @@ export function createStore({
|
|
|
73
106
|
|
|
74
107
|
/**
|
|
75
108
|
* Updates the state based on elapsed time and processes events.
|
|
109
|
+
*
|
|
110
|
+
* The current state is left untouched while the queued events are applied to a separate
|
|
111
|
+
* draft, according to the configured `updateStrategy`. The draft only replaces the current
|
|
112
|
+
* state once every queued event has been processed, and subscribers are notified afterwards.
|
|
113
|
+
*
|
|
114
|
+
* @returns {Event[]} The events processed during this update.
|
|
76
115
|
*/
|
|
77
116
|
function update() {
|
|
78
117
|
if (isProcessing) {
|
|
@@ -81,18 +120,37 @@ export function createStore({
|
|
|
81
120
|
|
|
82
121
|
isProcessing = true
|
|
83
122
|
const processedEvents = []
|
|
123
|
+
let nextState
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
if (updateStrategy === STRUCTURAL_SHARING) {
|
|
126
|
+
nextState = create(state, patch, {
|
|
127
|
+
enableAutoFreeze: state.game?.devMode,
|
|
128
|
+
})
|
|
129
|
+
} else {
|
|
130
|
+
const draft = structuredClone(state)
|
|
131
|
+
|
|
132
|
+
patch(draft)
|
|
133
|
+
nextState = draft
|
|
134
|
+
}
|
|
88
135
|
|
|
136
|
+
state = nextState
|
|
89
137
|
isProcessing = false
|
|
90
138
|
|
|
91
139
|
listeners.forEach((onUpdate) => onUpdate())
|
|
92
140
|
|
|
93
141
|
return processedEvents
|
|
94
142
|
|
|
95
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Applies every queued event to the given draft.
|
|
145
|
+
*
|
|
146
|
+
* The draft is either the proxy returned by `create` (structural sharing) or the
|
|
147
|
+
* deep copy produced by `structuredClone` (full clone). The current state is not
|
|
148
|
+
* modified, and only becomes the new state once this function returns.
|
|
149
|
+
*
|
|
150
|
+
* @param {Entities} draft - The draft to apply the queued events to.
|
|
151
|
+
* @returns {void}
|
|
152
|
+
*/
|
|
153
|
+
function patch(draft) {
|
|
96
154
|
while (incomingEvents.length) {
|
|
97
155
|
const event = incomingEvents.shift()
|
|
98
156
|
processedEvents.push(event)
|
|
@@ -193,7 +251,8 @@ export function createStore({
|
|
|
193
251
|
/**
|
|
194
252
|
* Sets an augmented type configuration given its name.
|
|
195
253
|
* @param {string} typeName - The name of the type to set.
|
|
196
|
-
* @param {
|
|
254
|
+
* @param {Type} type - The type configuration.
|
|
255
|
+
* @returns {void}
|
|
197
256
|
*/
|
|
198
257
|
function setType(typeName, type) {
|
|
199
258
|
const oldType = types[typeName]
|
|
@@ -229,7 +288,8 @@ export function createStore({
|
|
|
229
288
|
/**
|
|
230
289
|
* Sets the entire state of the store.
|
|
231
290
|
* This is useful for importing state or setting initial state from a server.
|
|
232
|
-
* @param {
|
|
291
|
+
* @param {Entities} nextState - The new state to set.
|
|
292
|
+
* @returns {void}
|
|
233
293
|
*/
|
|
234
294
|
function setState(nextState) {
|
|
235
295
|
const oldEntities = state ?? {}
|
|
@@ -289,11 +349,18 @@ export function createStore({
|
|
|
289
349
|
|
|
290
350
|
/**
|
|
291
351
|
* Resets the store to its initial state.
|
|
352
|
+
* @returns {void}
|
|
292
353
|
*/
|
|
293
354
|
function reset() {
|
|
294
355
|
setState(originalEntities)
|
|
295
356
|
}
|
|
296
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Adds an entity to the state and registers its event handlers.
|
|
360
|
+
* @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
|
|
361
|
+
* @param {Entity} payload - The entity to add, including its id.
|
|
362
|
+
* @returns {void}
|
|
363
|
+
*/
|
|
297
364
|
function addEntity(draft, payload) {
|
|
298
365
|
const { id, ...entity } = payload
|
|
299
366
|
draft[id] = augmentEntity(id, entity)
|
|
@@ -303,9 +370,40 @@ export function createStore({
|
|
|
303
370
|
incomingEvents.unshift({ type: `#${id}:create` })
|
|
304
371
|
}
|
|
305
372
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Queues the destruction of an entity. The entity is removed by the `#id:destroy` event.
|
|
375
|
+
* @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
|
|
376
|
+
* @param {string} id - The id of the entity to remove.
|
|
377
|
+
* @returns {void}
|
|
378
|
+
*/
|
|
379
|
+
function removeEntity(draft, id) {
|
|
309
380
|
incomingEvents.unshift({ type: `#${id}:destroy` })
|
|
310
381
|
}
|
|
311
382
|
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Validates the given update strategy.
|
|
386
|
+
* @param {UpdateStrategy} updateStrategy - The update strategy to validate.
|
|
387
|
+
* @returns {void}
|
|
388
|
+
* @throws {TypeError} If the update strategy is not supported.
|
|
389
|
+
*/
|
|
390
|
+
function assertUpdateStrategy(updateStrategy) {
|
|
391
|
+
if (UPDATE_STRATEGIES.includes(updateStrategy)) {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
throw new TypeError(
|
|
396
|
+
`Unsupported update strategy: ${updateStrategy}. Expected one of: ${UPDATE_STRATEGIES.join(", ")}.`,
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @typedef {"structural-sharing" | "full-clone"} UpdateStrategy - How the state is copied before events are applied.
|
|
402
|
+
* @typedef {Object} StoreConfig - Configuration options accepted by {@link createStore}.
|
|
403
|
+
* @typedef {Object.<string, any>} Type - An augmented entity type.
|
|
404
|
+
* @typedef {Object.<string, Type>} Types - A collection of augmented types.
|
|
405
|
+
* @typedef {Object.<string, any>} Entity - An object representing an entity.
|
|
406
|
+
* @typedef {Object.<string, Entity>} Entities - A collection of entities indexed by id.
|
|
407
|
+
* @typedef {(listener: () => void) => () => void} Listener - Subscribes to state updates and returns an unsubscribe function.
|
|
408
|
+
* @typedef {Object} Store - The store returned by {@link createStore}.
|
|
409
|
+
*/
|
package/src/types.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ensureArray } from "@inglorious/utils/data-structures/array.js"
|
|
2
|
-
import {
|
|
3
|
-
import { extend } from "@inglorious/utils/
|
|
4
|
-
import { pipe } from "@inglorious/utils/functions/functions.js"
|
|
2
|
+
import { pipe } from "@inglorious/utils/functions"
|
|
3
|
+
import { extend, map } from "@inglorious/utils/objects"
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Augments a single type by composing its behaviors and mixins.
|
package/types/store.d.ts
CHANGED
|
@@ -43,6 +43,28 @@ export type EntitiesState<TEntity extends BaseEntity = BaseEntity> = {
|
|
|
43
43
|
export type TypesConfig<TEntity extends BaseEntity = BaseEntity> = {
|
|
44
44
|
[typeName: string]: EntityType<TEntity>
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether each event triggers an update, or updates are batched until `update()` is called.
|
|
48
|
+
*/
|
|
49
|
+
export type UpdateMode = "auto" | "manual"
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How the state is copied before queued events are applied.
|
|
53
|
+
*
|
|
54
|
+
* - `"structural-sharing"` (default) applies events to a draft proxy, so only the entities that
|
|
55
|
+
* actually changed are copied. Unchanged entities keep their previous reference, which keeps
|
|
56
|
+
* reference-equality change detection cheap. Best for UI stores.
|
|
57
|
+
* - `"full-clone"` deep-clones the whole state with `structuredClone` and applies events to the
|
|
58
|
+
* copy without a proxy. Cost is proportional to the total state size per update, but there is
|
|
59
|
+
* no per-entity proxy overhead, so it scales better when thousands of entities change every
|
|
60
|
+
* frame, as in a game simulation.
|
|
61
|
+
*
|
|
62
|
+
* Under both strategies the current state is left intact while the queued events are applied to
|
|
63
|
+
* a separate draft, and the two are swapped only once every event has been processed. This means
|
|
64
|
+
* that during an update `api.getEntity()` and `api.getEntities()` read the previous state rather
|
|
65
|
+
* than the in-flight changes.
|
|
66
|
+
*/
|
|
67
|
+
export type UpdateStrategy = "structural-sharing" | "full-clone"
|
|
46
68
|
|
|
47
69
|
/**
|
|
48
70
|
* Store configuration
|
|
@@ -56,7 +78,8 @@ export interface StoreConfig<
|
|
|
56
78
|
systems?: System<TState>[]
|
|
57
79
|
middlewares?: Middleware<TEntity, TState>[]
|
|
58
80
|
autoCreateEntities?: boolean
|
|
59
|
-
|
|
81
|
+
updateMode?: UpdateMode
|
|
82
|
+
updateStrategy?: UpdateStrategy
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
/**
|