@inglorious/store 11.0.2 → 11.0.3
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 +112 -1
- package/package.json +3 -3
- package/src/api.js +5 -1
- package/src/client/devtools.js +1 -1
- package/src/client/multiplayer-middleware.js +14 -4
- package/src/entities.js +1 -1
- package/src/migration/rtk.js +1 -1
- package/src/store.js +171 -4
- package/src/types.js +1 -1
- package/types/store.d.ts +7 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Game engines solved state complexity years ago — Inglorious Store brings those
|
|
|
30
30
|
- ✅ Entity-based state (manage multiple instances effortlessly)
|
|
31
31
|
- ✅ No action creators, thunks, or slices
|
|
32
32
|
- ✅ Predictable, testable, purely functional code
|
|
33
|
-
- ✅ Built-in lifecycle events (`add`, `remove`)
|
|
33
|
+
- ✅ Built-in lifecycle events (`add`, `remove`, `patch`, `replace`)
|
|
34
34
|
- ✅ 10x faster immutability than Redux Toolkit (Mutative vs Immer)
|
|
35
35
|
|
|
36
36
|
---
|
|
@@ -364,6 +364,56 @@ Inglorious Store has a few built-in events that you can use:
|
|
|
364
364
|
|
|
365
365
|
- `add`: adds a new entity to the state. Triggers a `create` lifecycle event.
|
|
366
366
|
- `remove`: removes an entity from the state. Triggers a `destroy` lifecycle event.
|
|
367
|
+
- `patch`: changes some of a standing entity and leaves the rest alone.
|
|
368
|
+
- `replace`: installs a new configuration on a standing entity, dropping what it does
|
|
369
|
+
not mention.
|
|
370
|
+
|
|
371
|
+
`patch` and `replace` cover the case `add` and `remove` do not: changing something that is
|
|
372
|
+
already in the world without taking it out of it. They are the same pair the web settled
|
|
373
|
+
long ago — `POST`, `DELETE`, `PUT`, `PATCH` — for the same reason:
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
376
|
+
// A patch touches only what it names. Anything else the entity already had survives,
|
|
377
|
+
// which is what makes it safe to nudge part of something mid-flight.
|
|
378
|
+
store.notify("patch", { id: "counter1", value: 10 })
|
|
379
|
+
|
|
380
|
+
// A replace says the whole of it is different, so anything it does not name is gone.
|
|
381
|
+
store.notify("replace", { id: "counter1", type: "Counter", value: 10 })
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
A field set to `undefined` is set to _nothing_ in either, rather than being left alone:
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
// The key stops being mapped. This is not the same as saying nothing about it, which
|
|
388
|
+
// would leave whatever it was mapped to last time standing.
|
|
389
|
+
store.notify("patch", {
|
|
390
|
+
id: "keyboard",
|
|
391
|
+
mapping: { Escape: "pause", ArrowUp: undefined },
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Neither runs `create` or `destroy`: the entity never stopped standing, and something that
|
|
396
|
+
was never removed cannot have been destroyed. Naming a different `type` in either moves
|
|
397
|
+
the entity onto that type's handlers.
|
|
398
|
+
|
|
399
|
+
The practical use is a world whose rules change with its state — a keyboard whose keys
|
|
400
|
+
mean one thing on a menu and another in play:
|
|
401
|
+
|
|
402
|
+
```javascript
|
|
403
|
+
const types = {
|
|
404
|
+
Keyboard: { keyDown: (entity, key) => report(key) },
|
|
405
|
+
Game: [
|
|
406
|
+
scenes({
|
|
407
|
+
menu: () => [{ id: "keyboard", type: "Keyboard", mapping: MENU_KEYS }],
|
|
408
|
+
play: () => [{ id: "keyboard", type: "Keyboard", mapping: PLAY_KEYS }],
|
|
409
|
+
}),
|
|
410
|
+
fsm({/* … */}),
|
|
411
|
+
],
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The keyboard is one keyboard, not two: the mapping is patched when the state moves, and
|
|
416
|
+
`isDeepEqual` from `@inglorious/utils` is what tells the two apart.
|
|
367
417
|
|
|
368
418
|
The lifecycle events can be used to define event handlers similar to constructor and destructor methods in OOP:
|
|
369
419
|
|
|
@@ -634,6 +684,19 @@ The wrapper snapshots the keys in the patch, applies them in `Start`, restores t
|
|
|
634
684
|
|
|
635
685
|
---
|
|
636
686
|
|
|
687
|
+
### Reading a store from outside a handler
|
|
688
|
+
|
|
689
|
+
`api.getEntity` is the answer during a handler. Outside one — a test driving a world, or
|
|
690
|
+
anything else holding the store rather than an event — the store says the same thing
|
|
691
|
+
itself:
|
|
692
|
+
|
|
693
|
+
```javascript
|
|
694
|
+
store.getEntity("user") // one entity, or undefined
|
|
695
|
+
store.getState() // the lot
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
There is one lookup rather than two spellings of it: `api.getEntity` asks the store.
|
|
699
|
+
|
|
637
700
|
### Event scoping
|
|
638
701
|
|
|
639
702
|
By default, lifecycle events are **scoped to the triggering entity**:
|
|
@@ -1256,6 +1319,54 @@ Each handler receives three arguments:
|
|
|
1256
1319
|
|
|
1257
1320
|
- **`create(entity)`** - triggered when entity added via `add` event, visible only to that entity
|
|
1258
1321
|
- **`destroy(entity)`** - triggered when entity removed via `remove` event, visible only to that entity
|
|
1322
|
+
- **`pause`** - halts the world, see below
|
|
1323
|
+
- **`resume`** - unhalts it
|
|
1324
|
+
- **`add` / `remove`** - put a thing into the world or take it out, triggering `create` / `destroy`
|
|
1325
|
+
- **`patch` / `replace`** - change a thing that is already standing, the first touching only
|
|
1326
|
+
what it names and the second installing all of it
|
|
1327
|
+
- **`update`** - the frame tick, disabled while the world is paused
|
|
1328
|
+
|
|
1329
|
+
#### Pausing the world
|
|
1330
|
+
|
|
1331
|
+
`notify("pause")` stops the store handing out `update` events, and `notify("resume")` starts it again:
|
|
1332
|
+
|
|
1333
|
+
```javascript
|
|
1334
|
+
store.notify("pause")
|
|
1335
|
+
store.notify("update")
|
|
1336
|
+
store.update() // nothing updated
|
|
1337
|
+
|
|
1338
|
+
store.notify("resume")
|
|
1339
|
+
store.notify("update")
|
|
1340
|
+
store.update() // updated as normal
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
This is for anything that moves by integrating a delta time. Those entities do not need to know about pausing at all: their `update` is simply never called, so they cannot drift.
|
|
1344
|
+
|
|
1345
|
+
Everything else keeps flowing. Input handlers, custom events and rendering are untouched, which is what lets a pause screen take itself back off:
|
|
1346
|
+
|
|
1347
|
+
```javascript
|
|
1348
|
+
// Still works while the world is halted
|
|
1349
|
+
pauseMenu.pause(entity, _, api) {
|
|
1350
|
+
api.notify("resume")
|
|
1351
|
+
}
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
An entity opts out by setting `updatesWhilePaused`, which is for overlays and menus rather than for gameplay:
|
|
1355
|
+
|
|
1356
|
+
```javascript
|
|
1357
|
+
const Overlay = [{ update(entity, dt) { entity.ticks++ } }]
|
|
1358
|
+
|
|
1359
|
+
// in entities
|
|
1360
|
+
overlay: { type: "Overlay", updatesWhilePaused: true }
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
It is a property of the entity rather than of the type so that a type stays a plain index signature of handlers, which is what keeps it freely callable and indexable from TypeScript.
|
|
1364
|
+
|
|
1365
|
+
The default is to **stop**. Anything that keeps updating has to say so, so forgetting a flag pauses the thing you meant to pause rather than freezing the menu.
|
|
1366
|
+
|
|
1367
|
+
`reset()` clears the halt, so replacing the entities does not leave the world frozen.
|
|
1368
|
+
|
|
1369
|
+
Note that `pause` and `resume` keep dispatching like any other event, unlike `add` and `remove`. The store acts on them _and_ every type that handles them still gets called, so one notification can both halt the world and let the game record why.
|
|
1259
1370
|
|
|
1260
1371
|
### Notify vs Dispatch
|
|
1261
1372
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "11.0.
|
|
3
|
+
"version": "11.0.3",
|
|
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,11 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@inglorious/utils": "
|
|
58
|
+
"@inglorious/utils": "5.0.0",
|
|
59
59
|
"mutative": "^1.3.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@inglorious/utils": "
|
|
62
|
+
"@inglorious/utils": "5.0.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@inglorious/eslint-config": "1.1.2",
|
package/src/api.js
CHANGED
|
@@ -37,10 +37,14 @@ export function createApi(store, extras) {
|
|
|
37
37
|
},
|
|
38
38
|
/**
|
|
39
39
|
* Retrieves a single entity by ID.
|
|
40
|
+
*
|
|
41
|
+
* The store answers this for itself, so there is one lookup rather than two
|
|
42
|
+
* spellings of the same one.
|
|
43
|
+
*
|
|
40
44
|
* @param {string} id
|
|
41
45
|
* @returns {Object | undefined}
|
|
42
46
|
*/
|
|
43
|
-
getEntity:
|
|
47
|
+
getEntity: store.getEntity,
|
|
44
48
|
/**
|
|
45
49
|
* Runs a selector against the current state.
|
|
46
50
|
*
|
package/src/client/devtools.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { deserialize, extend, serialize } from "@inglorious/utils/
|
|
1
|
+
import { deserialize, extend, serialize } from "@inglorious/utils/object.js"
|
|
2
2
|
|
|
3
|
-
// A constant for the server's WebSocket URL.
|
|
4
|
-
const DEFAULT_SERVER_URL = `ws://${window.location.hostname}:3000`
|
|
5
3
|
const DEFAULT_RECONNECTION_DELAY = 1000
|
|
6
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Where the server is, when the caller has not said.
|
|
7
|
+
*
|
|
8
|
+
* Read when it is asked for rather than when this module is imported, so that importing it
|
|
9
|
+
* does not require a browser -- which is what lets the store be imported anywhere.
|
|
10
|
+
*
|
|
11
|
+
* @returns {string} The server's WebSocket URL.
|
|
12
|
+
*/
|
|
13
|
+
function defaultServerUrl() {
|
|
14
|
+
return `ws://${window.location.hostname}:3000`
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Creates and returns the multiplayer middleware.
|
|
9
19
|
* @returns {Function} The middleware function.
|
|
10
20
|
*/
|
|
11
21
|
export function multiplayerMiddleware(config = {}) {
|
|
12
|
-
const serverUrl = config.serverUrl ??
|
|
22
|
+
const serverUrl = config.serverUrl ?? defaultServerUrl()
|
|
13
23
|
const reconnectionDelay =
|
|
14
24
|
config.reconnectionDelay ?? DEFAULT_RECONNECTION_DELAY
|
|
15
25
|
|
package/src/entities.js
CHANGED
package/src/migration/rtk.js
CHANGED
package/src/store.js
CHANGED
|
@@ -63,7 +63,7 @@ export function createStore({
|
|
|
63
63
|
|
|
64
64
|
const types = augmentTypes(originalTypes)
|
|
65
65
|
|
|
66
|
-
let state, eventMap, incomingEvents, isProcessing
|
|
66
|
+
let state, eventMap, incomingEvents, isProcessing, isHalted
|
|
67
67
|
reset()
|
|
68
68
|
|
|
69
69
|
const baseStore = {
|
|
@@ -75,6 +75,7 @@ export function createStore({
|
|
|
75
75
|
getType,
|
|
76
76
|
setType,
|
|
77
77
|
getState,
|
|
78
|
+
getEntity,
|
|
78
79
|
setState,
|
|
79
80
|
reset,
|
|
80
81
|
}
|
|
@@ -118,6 +119,14 @@ export function createStore({
|
|
|
118
119
|
return []
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// Nothing queued means nothing to apply and nothing to publish: skipping the draft
|
|
123
|
+
// entirely, rather than copying the world just to arrive back where it started.
|
|
124
|
+
// This is what keeps a loop that pumps every animation frame cheap on the frames
|
|
125
|
+
// where neither the game nor the player had anything to say.
|
|
126
|
+
if (!incomingEvents.length) {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
isProcessing = true
|
|
122
131
|
const processedEvents = []
|
|
123
132
|
let nextState
|
|
@@ -147,6 +156,13 @@ export function createStore({
|
|
|
147
156
|
* deep copy produced by `structuredClone` (full clone). The current state is not
|
|
148
157
|
* modified, and only becomes the new state once this function returns.
|
|
149
158
|
*
|
|
159
|
+
* A change made by a handler is not visible to the next handler in the same pass:
|
|
160
|
+
* `getState` still returns the state as it was before the pass began. This is on
|
|
161
|
+
* purpose -- every event in the pass sees one consistent world, rather than some
|
|
162
|
+
* changes having landed and others not -- and it means a handler that asks what is
|
|
163
|
+
* left is asking about the past. Anything reacting to a change should be told the
|
|
164
|
+
* change rather than go looking for it.
|
|
165
|
+
*
|
|
150
166
|
* @param {Entities} draft - The draft to apply the queued events to.
|
|
151
167
|
* @returns {void}
|
|
152
168
|
*/
|
|
@@ -155,15 +171,64 @@ export function createStore({
|
|
|
155
171
|
const event = incomingEvents.shift()
|
|
156
172
|
processedEvents.push(event)
|
|
157
173
|
|
|
174
|
+
// Pausing and resuming are the store's own, and they keep dispatching like any
|
|
175
|
+
// other event, so the game behaviour still sees them and anything else that
|
|
176
|
+
// wants to react gets the same notification.
|
|
177
|
+
if (event.type === "pause") {
|
|
178
|
+
isHalted = true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (event.type === "resume") {
|
|
182
|
+
isHalted = false
|
|
183
|
+
}
|
|
184
|
+
|
|
158
185
|
// Handle special system events
|
|
186
|
+
// `add` and `remove` change the world and are then dispatched like any
|
|
187
|
+
// other event, so that whatever is watching can hear about it. They are
|
|
188
|
+
// broadcast: the entity named by the payload is not the only one that wants
|
|
189
|
+
// to know, and something counting down what it put up needs to hear about
|
|
190
|
+
// each of them going.
|
|
191
|
+
//
|
|
192
|
+
// They are not the counterpart to `create` and `destroy`, which reach only
|
|
193
|
+
// the entity they are about so that those read as a constructor and a
|
|
194
|
+
// destructor without every handler beginning by checking whether it is the
|
|
195
|
+
// one being talked about.
|
|
196
|
+
//
|
|
197
|
+
// The two carry different payloads, and that is on purpose: a payload
|
|
198
|
+
// carries the least that can be had. A removal needs only the id, because
|
|
199
|
+
// everything else about the entity can still be looked up by it. An
|
|
200
|
+
// addition carries the entity itself, because there is nothing yet to look
|
|
201
|
+
// it up by -- the same asymmetry as removing a user by id but updating one
|
|
202
|
+
// by the patched user.
|
|
159
203
|
if (event.type === "add") {
|
|
160
204
|
addEntity(draft, event.payload)
|
|
161
|
-
continue
|
|
162
205
|
}
|
|
163
206
|
|
|
164
207
|
if (event.type === "remove") {
|
|
165
208
|
removeEntity(draft, event.payload)
|
|
166
|
-
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// A thing that stands through a change of state can still be told a different
|
|
212
|
+
// configuration, in either of the two ways the world offers.
|
|
213
|
+
//
|
|
214
|
+
// `patch` changes some of it and leaves the rest alone, which is how a screen
|
|
215
|
+
// takes over one field of something it shares with the screen before it. A field
|
|
216
|
+
// named as `undefined` is set to nothing rather than being left alone, which is
|
|
217
|
+
// how a key is taken off a mapping.
|
|
218
|
+
//
|
|
219
|
+
// `replace` installs what it is given and drops everything it does not mention,
|
|
220
|
+
// which is how the whole of something is said to be different.
|
|
221
|
+
//
|
|
222
|
+
// Neither takes the thing out of the world, so neither runs `create` or `destroy`:
|
|
223
|
+
// it never stopped standing, and a thing that was never removed cannot have been
|
|
224
|
+
// destroyed. This is the same pair the world has to hand for anything else --
|
|
225
|
+
// `add` and `remove` beside them, and post/delete/put/patch before that.
|
|
226
|
+
if (event.type === "patch") {
|
|
227
|
+
patchEntity(draft, event.payload)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (event.type === "replace") {
|
|
231
|
+
replaceEntity(draft, event.payload)
|
|
167
232
|
}
|
|
168
233
|
|
|
169
234
|
// Parse the event to get handler name
|
|
@@ -172,9 +237,21 @@ export function createStore({
|
|
|
172
237
|
// Get entities that should handle this event (filtered by EventMap)
|
|
173
238
|
const entityIds = eventMap.getEntitiesForEvent(event.type)
|
|
174
239
|
|
|
240
|
+
// A halted world stops handing out updates, because anything that moves by
|
|
241
|
+
// integrating a delta time would otherwise carry on regardless. An entity that
|
|
242
|
+
// sets `updatesWhilePaused` is exempt, which is how an overlay, a button or the
|
|
243
|
+
// thing that takes the pause back off keeps working while the rest stands still.
|
|
244
|
+
//
|
|
245
|
+
// It is read off the entity rather than the type so that the type stays an index
|
|
246
|
+
// signature of handlers, which is what lets it be called and indexed freely.
|
|
247
|
+
const halted = isHalted && handlerName === "update"
|
|
248
|
+
|
|
175
249
|
for (const id of entityIds) {
|
|
176
250
|
const entity = draft[id]
|
|
177
251
|
const type = types[entity.type]
|
|
252
|
+
|
|
253
|
+
if (halted && !entity.updatesWhilePaused) continue
|
|
254
|
+
|
|
178
255
|
const handle = type[handlerName]
|
|
179
256
|
|
|
180
257
|
handle?.(entity, event.payload, api)
|
|
@@ -209,8 +286,14 @@ export function createStore({
|
|
|
209
286
|
* - 'form:submit' - only form entities
|
|
210
287
|
* - 'form[loginForm]:submit' - only loginForm entity
|
|
211
288
|
*
|
|
289
|
+
* Two of these are the store's own. 'pause' stops the store handing out 'update'
|
|
290
|
+
* events, and 'resume' starts it again, which is how anything that moves by
|
|
291
|
+
* integrating a delta time stops without being told about it. A type declares
|
|
292
|
+
* `updatesWhilePaused` to keep updating regardless. Both keep dispatching to types
|
|
293
|
+
*
|
|
212
294
|
* @param {string} type - The event type to notify.
|
|
213
|
-
* @param {any} payload - The event payload.
|
|
295
|
+
* @param {any} payload - The event payload. Carries the least that can be had
|
|
296
|
+
* about what happened.
|
|
214
297
|
*/
|
|
215
298
|
function notify(type, payload) {
|
|
216
299
|
// NOTE: it's important to invoke store.dispatch instead of dispatch, otherwise we cannot override it
|
|
@@ -285,6 +368,20 @@ export function createStore({
|
|
|
285
368
|
return state
|
|
286
369
|
}
|
|
287
370
|
|
|
371
|
+
/**
|
|
372
|
+
* One entity by its id, or `undefined` if there is no such entity.
|
|
373
|
+
*
|
|
374
|
+
* This is what `api.getEntity` answers with during a handler, exposed here for the
|
|
375
|
+
* code that runs outside one: a test looking at a world it is driving, or anything else
|
|
376
|
+
* holding the store rather than an event's `api`.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} id - The entity's id.
|
|
379
|
+
* @returns {Object|undefined} The entity, as the last finished pass left it.
|
|
380
|
+
*/
|
|
381
|
+
function getEntity(id) {
|
|
382
|
+
return state?.[id]
|
|
383
|
+
}
|
|
384
|
+
|
|
288
385
|
/**
|
|
289
386
|
* Sets the entire state of the store.
|
|
290
387
|
* This is useful for importing state or setting initial state from a server.
|
|
@@ -352,6 +449,7 @@ export function createStore({
|
|
|
352
449
|
* @returns {void}
|
|
353
450
|
*/
|
|
354
451
|
function reset() {
|
|
452
|
+
isHalted = false
|
|
355
453
|
setState(originalEntities)
|
|
356
454
|
}
|
|
357
455
|
|
|
@@ -370,6 +468,75 @@ export function createStore({
|
|
|
370
468
|
incomingEvents.unshift({ type: `#${id}:create` })
|
|
371
469
|
}
|
|
372
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Moves a standing thing onto the handlers of whatever type it now names.
|
|
473
|
+
*
|
|
474
|
+
* A type is the only thing about a thing that cannot be merged: there is no such thing
|
|
475
|
+
* as being half a sort, so naming a different one means taking the thing out of the old
|
|
476
|
+
* one's list of entities before putting it in the new one.
|
|
477
|
+
*
|
|
478
|
+
* @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
|
|
479
|
+
* @param {string} id - The thing being moved.
|
|
480
|
+
* @param {string} previousType - The type it was under.
|
|
481
|
+
* @param {string|undefined} type - The type it is under now, if it has named one.
|
|
482
|
+
* @returns {void}
|
|
483
|
+
*/
|
|
484
|
+
function moveToType(draft, id, previousType, type) {
|
|
485
|
+
if (!type || type === previousType) return
|
|
486
|
+
|
|
487
|
+
eventMap.removeEntity(id, types[previousType], previousType)
|
|
488
|
+
eventMap.addEntity(id, types[type], type)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Merges a new configuration into a thing that is already standing.
|
|
493
|
+
*
|
|
494
|
+
* Only the fields the configuration names are touched, so anything the world has done
|
|
495
|
+
* to the thing since it was made survives unless the
|
|
496
|
+
* new configuration says otherwise. A field named as `undefined` is set to nothing,
|
|
497
|
+
* which is what taking a key off a mapping means.
|
|
498
|
+
*
|
|
499
|
+
* @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
|
|
500
|
+
* @param {Entity} payload - The thing and the fields to merge into it.
|
|
501
|
+
* @returns {void}
|
|
502
|
+
*/
|
|
503
|
+
function patchEntity(draft, payload) {
|
|
504
|
+
const { id, ...config } = payload
|
|
505
|
+
const existing = draft[id]
|
|
506
|
+
|
|
507
|
+
// Configuring something that is not there is not a thing that can be done. The world
|
|
508
|
+
// is built in one pass, so a scene naming a thing another scene is yet to put up is
|
|
509
|
+
// ordinary rather than a mistake, and is not worth failing over.
|
|
510
|
+
if (!existing) return
|
|
511
|
+
|
|
512
|
+
draft[id] = augmentEntity(id, { ...existing, ...config })
|
|
513
|
+
|
|
514
|
+
moveToType(draft, id, existing.type, config.type)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Installs a new configuration on a standing thing, dropping what it does not mention.
|
|
519
|
+
*
|
|
520
|
+
* The thing keeps its place in the world and its name; everything else is whatever it
|
|
521
|
+
* is told, and anything it is not told is gone. A field set to `undefined` is set to
|
|
522
|
+
* nothing here as much as it is in a patch -- the difference between the two is what
|
|
523
|
+
* happens to the fields neither mentions.
|
|
524
|
+
*
|
|
525
|
+
* @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
|
|
526
|
+
* @param {Entity} payload - The thing and the fields it is to be.
|
|
527
|
+
* @returns {void}
|
|
528
|
+
*/
|
|
529
|
+
function replaceEntity(draft, payload) {
|
|
530
|
+
const { id, ...config } = payload
|
|
531
|
+
const existing = draft[id]
|
|
532
|
+
|
|
533
|
+
if (!existing) return
|
|
534
|
+
|
|
535
|
+
draft[id] = augmentEntity(id, config)
|
|
536
|
+
|
|
537
|
+
moveToType(draft, id, existing.type, config.type)
|
|
538
|
+
}
|
|
539
|
+
|
|
373
540
|
/**
|
|
374
541
|
* Queues the destruction of an entity. The entity is removed by the `#id:destroy` event.
|
|
375
542
|
* @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
|
package/src/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureArray } from "@inglorious/utils/data-structures/array.js"
|
|
2
2
|
import { pipe } from "@inglorious/utils/functions"
|
|
3
|
-
import { extend, map } from "@inglorious/utils/
|
|
3
|
+
import { extend, map } from "@inglorious/utils/object.js"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Augments a single type by composing its behaviors and mixins.
|
package/types/store.d.ts
CHANGED
|
@@ -17,7 +17,12 @@ export interface Event<T = any> {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Entity type definition with event handlers
|
|
20
|
+
* Entity type definition with event handlers.
|
|
21
|
+
*
|
|
22
|
+
* `updatesWhilePaused` opts the type out of a pause: the store stops handing out
|
|
23
|
+
* `update` events while the world is halted, and a type that says this keeps
|
|
24
|
+
* receiving them. It is for overlays and menus; anything that moves should leave it
|
|
25
|
+
* off so that it stops.
|
|
21
26
|
*/
|
|
22
27
|
export type EntityType<TEntity extends BaseEntity = BaseEntity> = {
|
|
23
28
|
[K: string]: (entity: TEntity, payload: any, api: Api) => void
|
|
@@ -107,6 +112,7 @@ export interface Store<
|
|
|
107
112
|
getType: (typeName: string) => EntityType<TEntity>
|
|
108
113
|
setType: (typeName: string, type: EntityType<TEntity>) => void
|
|
109
114
|
getState: () => TState
|
|
115
|
+
getEntity: (id: string) => TState[string] | undefined
|
|
110
116
|
setState: (nextState: TState) => void
|
|
111
117
|
reset: () => void
|
|
112
118
|
_api?: Api<TEntity, TState>
|