Skip to content

Commit 09388d2

Browse files
committed
Migrate to immutability pattern
1 parent 80992c8 commit 09388d2

File tree

5 files changed

+120
-80
lines changed

5 files changed

+120
-80
lines changed

build.mjs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,15 @@
1616

1717
import esbuild, { analyzeMetafile } from "esbuild"
1818

19-
const sharedSettings = {
19+
const esm = await esbuild.build({
2020
entryPoints: ["src/index.ts"],
2121
bundle: true,
2222
write: true,
2323
outdir: "build",
2424
metafile: true,
2525
sourcemap: true,
26-
}
27-
28-
const esm = await esbuild.build({
29-
...sharedSettings,
3026
format: "esm",
27+
external: ["mutative"],
3128
})
3229

3330
console.log(await analyzeMetafile(esm.metafile))
34-
35-
const cjs = await esbuild.build({
36-
...sharedSettings,
37-
format: "cjs",
38-
entryNames: "index-cjs"
39-
})
40-
41-
console.log(await analyzeMetafile(cjs.metafile))

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
"name": "@peacockproject/statemachine-parser",
33
"version": "6.2.0",
44
"description": "IOI state machine conditional parser.",
5-
"main": "build/index-cjs.js",
6-
"module": "build/index.js",
5+
"type": "module",
6+
"main": "build/index.js",
77
"types": "build/index.d.ts",
88
"homepage": "https://thepeacockproject.org",
99
"sideEffects": false,
1010
"engines": {
11-
"node": ">=16.3.0"
11+
"node": ">=20"
1212
},
1313
"directories": {
1414
"test": "tests",
@@ -65,5 +65,8 @@
6565
},
6666
"publishConfig": {
6767
"access": "public"
68+
},
69+
"dependencies": {
70+
"mutative": "^1.3.0"
6871
}
6972
}

src/handleEvent.ts

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { handleActions, test } from "./index"
18-
import { deepClone, findNamedChild, set } from "./utils"
17+
import { test, handleActionsOnDraft } from "./index"
18+
import { findNamedChild } from "./utils"
1919
import {
2020
HandleEventOptions,
2121
HandleEventReturn,
2222
InStateEventHandler,
2323
StateMachineLike,
2424
} from "./types"
25+
import { create } from "mutative"
26+
2527

2628
/**
2729
* This function simulates an event happening, as if in game.
@@ -63,7 +65,7 @@ export function handleEvent<Context = unknown, Event = unknown>(
6365
const hasTimerState = !!csObject.$timer
6466

6567
// ensure no circular references are present, and that this won't update the param by accident
66-
let newContext = deepClone(context)
68+
let newContext = context
6769

6870
const doEventHandler = (handler: InStateEventHandler) => {
6971
// do we need to check conditions?
@@ -100,12 +102,17 @@ export function handleEvent<Context = unknown, Event = unknown>(
100102
},
101103
{
102104
pushUniqueAction(reference, item) {
103-
const referenceArray = findNamedChild(
104-
reference,
105-
newContext,
106-
true,
107-
)
108-
item = findNamedChild(item, newContext, false)
105+
const workingContext = {
106+
...newContext,
107+
...(definition.Constants || {}),
108+
...(options.contractId && {
109+
ContractId: options.contractId,
110+
}),
111+
Value: event,
112+
}
113+
114+
const referenceArray = findNamedChild(reference, workingContext, true)
115+
item = findNamedChild(item, workingContext, false)
109116
log(
110117
"action",
111118
`Running pushUniqueAction on ${reference} with ${item}`,
@@ -119,9 +126,13 @@ export function handleEvent<Context = unknown, Event = unknown>(
119126
return false
120127
}
121128

122-
referenceArray.push(item)
123-
124-
set(newContext, reference, referenceArray)
129+
// Actually modify the context using Mutative
130+
newContext = create(newContext, (draft) => {
131+
const draftArray = findNamedChild(reference, draft, true)
132+
if (Array.isArray(draftArray) && !draftArray.includes(item)) {
133+
draftArray.push(item)
134+
}
135+
})
125136

126137
return true
127138
},
@@ -147,43 +158,52 @@ export function handleEvent<Context = unknown, Event = unknown>(
147158
)
148159
}
149160

150-
for (const actionSet of Actions as unknown[]) {
151-
for (const action of Object.keys(actionSet)) {
152-
newContext = handleActions(
153-
{
154-
[action]: actionSet[action],
155-
},
156-
{
157-
...newContext,
158-
...(definition.Constants || {}),
159-
...(options.contractId && {
160-
ContractId: options.contractId,
161-
}),
162-
Value: event,
163-
},
164-
{
165-
originalContext: definition.Context ?? {},
166-
},
167-
)
168-
}
161+
// Special case: if the handler itself contains action keys and no Actions property,
162+
// treat the handler as the actions
163+
if (!handler.Actions && hasIrregularEventKeys) {
164+
Actions = [handler]
169165
}
170166

171-
// drop this specific event's value
172-
if (newContext.hasOwnProperty("Value")) {
173-
// @ts-expect-error
174-
delete newContext.Value
167+
// Create a working context with constants and event data
168+
const workingContext = {
169+
...newContext,
170+
...(definition.Constants || {}),
171+
...(options.contractId && {
172+
ContractId: options.contractId,
173+
}),
174+
Value: event,
175175
}
176176

177-
// drop this specific event's ContractId
178-
if (newContext.hasOwnProperty("ContractId")) {
179-
// @ts-expect-error
180-
delete newContext.ContractId
181-
}
177+
newContext = create(workingContext, (draft) => {
178+
for (const actionSet of Actions as unknown[]) {
179+
// For each action in the action set, apply it individually
180+
for (const actionKey of Object.keys(actionSet)) {
181+
handleActionsOnDraft(
182+
{ [actionKey]: actionSet[actionKey] },
183+
draft,
184+
draft, // Use the draft for reading values so each action sees previous changes
185+
{
186+
originalContext: definition.Context ?? {}
187+
}
188+
)
189+
}
190+
}
182191

183-
// drop the constants
184-
for (const constantKey of constantKeys) {
185-
delete newContext[constantKey]
186-
}
192+
// drop this specific event's value
193+
if (draft.hasOwnProperty("Value")) {
194+
delete draft.Value
195+
}
196+
197+
// drop this specific event's ContractId
198+
if (draft.hasOwnProperty("ContractId")) {
199+
delete draft.ContractId
200+
}
201+
202+
// drop the constants
203+
for (const constantKey of constantKeys) {
204+
delete draft[constantKey]
205+
}
206+
})
187207
}
188208

189209
let state = currentState

src/index.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { handleEvent } from "./handleEvent"
1818
import { HandleActionsOptions, TestOptions } from "./types"
1919
import { deepClone, findNamedChild, set } from "./utils"
2020
import { handleArrayLogic } from "./arrayHandling"
21+
import { create } from "mutative"
2122

2223
/**
2324
* Recursively evaluate a value or object.
@@ -240,6 +241,7 @@ function realTest<Variables>(
240241
path,
241242
}
242243

244+
// Add the timer directly to the array
243245
options.timers.push(timer)
244246
}
245247

@@ -254,6 +256,7 @@ function realTest<Variables>(
254256
)
255257

256258
if (index !== -1) {
259+
// Remove the timer directly from the array
257260
options.timers.splice(index, 1)
258261
}
259262

@@ -333,33 +336,52 @@ export function handleActions<Context>(
333336
return context
334337
}
335338

339+
return create(context, (draft) => {
340+
handleActionsOnDraft(input, draft, context, options)
341+
})
342+
}
343+
344+
/**
345+
* Internal function that applies actions directly to a draft object.
346+
* This is used both by handleActions and handleEvent to avoid code duplication.
347+
*/
348+
export function handleActionsOnDraft<Context>(
349+
input: any,
350+
draft: any,
351+
originalContext: Context,
352+
options?: HandleActionsOptions,
353+
): void {
354+
if (!input || typeof input !== "object") {
355+
return
356+
}
357+
336358
const addOrDec = (op: string) => {
337359
if (typeof input[op] === "string") {
338360
let reference = input[op]
339361

340-
const variableValue = findNamedChild(input[op], context, true)
362+
const variableValue = findNamedChild(input[op], originalContext, true)
341363

342364
if (typeof variableValue !== "number") {
343365
return
344366
}
345367

346368
set(
347-
context,
369+
draft,
348370
reference,
349371
op === "$inc" ? variableValue + 1 : variableValue - 1,
350372
)
351373
} else {
352374
let reference = input[op][0]
353375

354-
const variableValue = findNamedChild(reference, context, true)
355-
const incrementBy = findNamedChild(input[op][1], context, false)
376+
const variableValue = findNamedChild(reference, originalContext, true)
377+
const incrementBy = findNamedChild(input[op][1], originalContext, false)
356378

357379
if (typeof variableValue !== "number") {
358380
return
359381
}
360382

361383
set(
362-
context,
384+
draft,
363385
reference,
364386
op === "$inc"
365387
? variableValue + incrementBy
@@ -376,10 +398,10 @@ export function handleActions<Context>(
376398
reference = reference.substring(1)
377399
}
378400

379-
const value = findNamedChild(input[op][1], context, false)
401+
const value = findNamedChild(input[op][1], originalContext, false)
380402

381403
// clone the thing
382-
const array = deepClone(findNamedChild(reference, context, true))
404+
const array = deepClone(findNamedChild(reference, originalContext, true))
383405

384406
if (!Array.isArray(array)) {
385407
return
@@ -395,7 +417,7 @@ export function handleActions<Context>(
395417
array.push(value)
396418
}
397419

398-
set(context, reference, array)
420+
set(draft, reference, array)
399421
}
400422

401423
for (const key of Object.keys(input)) {
@@ -416,12 +438,12 @@ export function handleActions<Context>(
416438
// Therefore the 1st operand might get written to, but the 2nd one is purely a read.
417439
const variableValue1 = findNamedChild(
418440
input["$mul"][0],
419-
context,
441+
originalContext,
420442
true,
421443
)
422444
const variableValue2 = findNamedChild(
423445
input["$mul"][1],
424-
context,
446+
originalContext,
425447
false,
426448
)
427449

@@ -432,15 +454,15 @@ export function handleActions<Context>(
432454
break
433455
}
434456

435-
set(context, reference, variableValue1 * variableValue2)
457+
set(draft, reference, variableValue1 * variableValue2)
436458
break
437459
}
438460
case "$set": {
439461
let reference = input.$set[0]
440462

441-
const value = findNamedChild(input.$set[1], context, false)
463+
const value = findNamedChild(input.$set[1], originalContext, false)
442464

443-
set(context, reference, value)
465+
set(draft, reference, value)
444466
break
445467
}
446468
case "$push": {
@@ -458,11 +480,11 @@ export function handleActions<Context>(
458480
reference = reference.substring(1)
459481
}
460482

461-
const value = findNamedChild(input.$remove[1], context, false)
483+
const value = findNamedChild(input.$remove[1], originalContext, false)
462484

463485
// clone the thing
464486
let array: unknown[] = deepClone(
465-
findNamedChild(reference, context, true),
487+
findNamedChild(reference, originalContext, true),
466488
)
467489

468490
if (!Array.isArray(array)) {
@@ -471,24 +493,22 @@ export function handleActions<Context>(
471493

472494
array = array.filter((item) => item !== value)
473495

474-
set(context, reference, array)
496+
set(draft, reference, array)
475497
break
476498
}
477499
case "$reset": {
478500
let reference = input.$reset
479501
const value = findNamedChild(
480502
reference,
481-
options.originalContext,
503+
options?.originalContext,
482504
true,
483505
)
484506

485-
set(context, reference, value)
507+
set(draft, reference, value)
486508
break
487509
}
488510
}
489511
}
490-
491-
return context
492512
}
493513

494514
export { handleEvent }

0 commit comments

Comments
 (0)