@routevn/creator-model 1.2.7 → 1.2.8

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 CHANGED
@@ -42,6 +42,11 @@ processCommand({
42
42
  state,
43
43
  command: { type, payload },
44
44
  });
45
+
46
+ replayCommands({
47
+ state,
48
+ commands: [{ type, payload }],
49
+ });
45
50
  ```
46
51
 
47
52
  `SCHEMA_VERSION` is the exported schema version constant for persisted command
@@ -76,6 +81,12 @@ or:
76
81
  { valid: true, state: nextState }
77
82
  ```
78
83
 
84
+ `replayCommands()` returns the same result shape while applying a trusted
85
+ command tape against a single working state clone. It is intended for batch
86
+ history reconstruction paths such as offline repository boot/import replay,
87
+ where validating the full state before and after every individual command is
88
+ needlessly expensive.
89
+
79
90
  Design rules:
80
91
 
81
92
  - no classes
@@ -86,6 +97,7 @@ Design rules:
86
97
  - patch releases must not change persisted schema compatibility
87
98
  - `bun run test:compat` is the required compatibility gate for model changes
88
99
  - `processCommand()` is the authoritative state transition
100
+ - `replayCommands()` is the fast batch replay path for trusted command tapes
89
101
  - model state should contain project-owned runtime data only
90
102
  - app-owned metadata like project id, name, and description should stay out of
91
103
  this package
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routevn/creator-model",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export {
2
+ replayCommands,
2
3
  SCHEMA_VERSION,
3
4
  processCommand,
4
5
  validateAgainstState,
package/src/model.js CHANGED
@@ -15478,50 +15478,118 @@ export const validatePayload = ({ type, payload }) => {
15478
15478
  });
15479
15479
  };
15480
15480
 
15481
+ const normalizeCurrentStateValidationResult = (stateResult) => {
15482
+ if (stateResult.valid) {
15483
+ return stateResult;
15484
+ }
15485
+
15486
+ if (stateResult.error.kind === "invariant") {
15487
+ return invalidInvariant(
15488
+ stateResult.error.message,
15489
+ toDomainErrorDetails(stateResult.error),
15490
+ );
15491
+ }
15492
+
15493
+ return invalidState(
15494
+ stateResult.error.message,
15495
+ toDomainErrorDetails(stateResult.error),
15496
+ );
15497
+ };
15498
+
15499
+ const validateCommandDefinitionAgainstState = ({ state, command }) => {
15500
+ if (!isPlainObject(command)) {
15501
+ return invalidPrecondition("command must be an object");
15502
+ }
15503
+
15504
+ const payloadResult = validatePayload(command);
15505
+ if (!payloadResult.valid) {
15506
+ return invalidPayload(
15507
+ payloadResult.error.message,
15508
+ toDomainErrorDetails(payloadResult.error),
15509
+ );
15510
+ }
15511
+
15512
+ const definition = getCommandDefinition({ type: command.type });
15513
+ if (!definition) {
15514
+ return invalidPrecondition(`unknown command type '${command.type}'`);
15515
+ }
15516
+
15517
+ const validationResult = captureValidation(() =>
15518
+ definition.validateAgainstState({
15519
+ state,
15520
+ payload: command.payload,
15521
+ }),
15522
+ );
15523
+ const normalizedValidationResult = normalizeStateResult(validationResult);
15524
+ if (!normalizedValidationResult.valid) {
15525
+ return normalizedValidationResult;
15526
+ }
15527
+
15528
+ return {
15529
+ valid: true,
15530
+ definition,
15531
+ };
15532
+ };
15533
+
15534
+ const applyCommandDefinition = ({ state, definition, payload }) => {
15535
+ const nextState = definition.reduce({
15536
+ state,
15537
+ payload,
15538
+ });
15539
+ if (nextState?.valid === false) {
15540
+ return nextState;
15541
+ }
15542
+
15543
+ return {
15544
+ valid: true,
15545
+ state: nextState === undefined ? state : nextState,
15546
+ };
15547
+ };
15548
+
15549
+ const appendReplayCommandContext = (result, { commandIndex, command } = {}) => {
15550
+ if (result?.valid !== false || !result.error) {
15551
+ return result;
15552
+ }
15553
+
15554
+ const details = isPlainObject(result.error.details)
15555
+ ? structuredClone(result.error.details)
15556
+ : {};
15557
+
15558
+ if (Number.isInteger(commandIndex) && commandIndex >= 0) {
15559
+ details.commandIndex = commandIndex;
15560
+ }
15561
+
15562
+ if (isNonEmptyString(command?.type)) {
15563
+ details.commandType = command.type;
15564
+ }
15565
+
15566
+ return {
15567
+ valid: false,
15568
+ error: {
15569
+ ...result.error,
15570
+ details,
15571
+ },
15572
+ };
15573
+ };
15574
+
15481
15575
  export const validateAgainstState = ({ state, command }) => {
15482
15576
  return captureValidation(() => {
15483
15577
  const normalizedState = normalizeStateCollections(state);
15484
15578
 
15485
- if (!isPlainObject(command)) {
15486
- return invalidPrecondition("command must be an object");
15487
- }
15488
-
15489
- const payloadResult = validatePayload(command);
15490
- if (!payloadResult.valid) {
15491
- return invalidPayload(
15492
- payloadResult.error.message,
15493
- toDomainErrorDetails(payloadResult.error),
15494
- );
15495
- }
15496
-
15497
15579
  const stateResult = validateState({ state: normalizedState });
15498
15580
  if (!stateResult.valid) {
15499
- if (stateResult.error.kind === "invariant") {
15500
- return invalidInvariant(
15501
- stateResult.error.message,
15502
- toDomainErrorDetails(stateResult.error),
15503
- );
15504
- }
15505
-
15506
- return invalidState(
15507
- stateResult.error.message,
15508
- toDomainErrorDetails(stateResult.error),
15509
- );
15581
+ return normalizeCurrentStateValidationResult(stateResult);
15510
15582
  }
15511
15583
 
15512
- const definition = getCommandDefinition({ type: command.type });
15513
- if (!definition) {
15514
- return invalidPrecondition(`unknown command type '${command.type}'`);
15584
+ const validationResult = validateCommandDefinitionAgainstState({
15585
+ state: normalizedState,
15586
+ command,
15587
+ });
15588
+ if (!validationResult.valid) {
15589
+ return validationResult;
15515
15590
  }
15516
15591
 
15517
- const validationResult = captureValidation(() =>
15518
- definition.validateAgainstState({
15519
- state: normalizedState,
15520
- payload: command.payload,
15521
- }),
15522
- );
15523
-
15524
- return normalizeStateResult(validationResult);
15592
+ return VALID_RESULT;
15525
15593
  });
15526
15594
  };
15527
15595
 
@@ -15534,49 +15602,97 @@ export const processCommand = ({ state, command }) => {
15534
15602
  command.type.startsWith("spritesheet.") ||
15535
15603
  command.type.startsWith("particle."));
15536
15604
 
15537
- if (!isPlainObject(command)) {
15538
- return invalidPrecondition("command must be an object");
15605
+ const stateResult = validateState({ state: normalizedState });
15606
+ if (!stateResult.valid) {
15607
+ return normalizeCurrentStateValidationResult(stateResult);
15539
15608
  }
15540
15609
 
15541
- const preconditionResult = validateAgainstState({
15610
+ const validationResult = validateCommandDefinitionAgainstState({
15542
15611
  state: normalizedState,
15543
15612
  command,
15544
15613
  });
15545
- if (!preconditionResult.valid) {
15546
- return preconditionResult;
15614
+ if (!validationResult.valid) {
15615
+ return validationResult;
15547
15616
  }
15548
15617
 
15549
- const definition = getCommandDefinition({ type: command.type });
15550
- if (!definition) {
15551
- return invalidPrecondition(`unknown command type '${command.type}'`);
15552
- }
15553
-
15554
- const nextState = definition.reduce({
15618
+ const applyResult = applyCommandDefinition({
15555
15619
  state: structuredClone(
15556
15620
  shouldMaterializeNormalizedState ? normalizedState : state,
15557
15621
  ),
15622
+ definition: validationResult.definition,
15558
15623
  payload: command.payload,
15559
15624
  });
15560
- if (nextState?.valid === false) {
15561
- return nextState;
15625
+ if (!applyResult.valid) {
15626
+ return applyResult;
15627
+ }
15628
+
15629
+ const stateResultAfterCommand = validateState({
15630
+ state: applyResult.state,
15631
+ });
15632
+ if (!stateResultAfterCommand.valid) {
15633
+ return stateResultAfterCommand;
15562
15634
  }
15563
15635
 
15564
- const finalState =
15565
- nextState === undefined
15566
- ? shouldMaterializeNormalizedState
15567
- ? normalizedState
15568
- : state
15569
- : nextState;
15636
+ return {
15637
+ valid: true,
15638
+ state: applyResult.state,
15639
+ };
15640
+ });
15641
+ };
15642
+
15643
+ export const replayCommands = ({ state, commands }) => {
15644
+ return captureValidation(() => {
15645
+ if (!Array.isArray(commands)) {
15646
+ return invalidPrecondition("commands must be an array");
15647
+ }
15648
+
15649
+ const normalizedState = normalizeStateCollections(state);
15570
15650
  const stateResult = validateState({
15571
- state: finalState,
15651
+ state: normalizedState,
15572
15652
  });
15573
15653
  if (!stateResult.valid) {
15574
- return stateResult;
15654
+ return normalizeCurrentStateValidationResult(stateResult);
15655
+ }
15656
+
15657
+ let workingState = structuredClone(normalizedState);
15658
+ for (let index = 0; index < commands.length; index += 1) {
15659
+ const command = commands[index];
15660
+ const validationResult = validateCommandDefinitionAgainstState({
15661
+ state: workingState,
15662
+ command,
15663
+ });
15664
+ if (!validationResult.valid) {
15665
+ return appendReplayCommandContext(validationResult, {
15666
+ commandIndex: index,
15667
+ command,
15668
+ });
15669
+ }
15670
+
15671
+ const applyResult = applyCommandDefinition({
15672
+ state: workingState,
15673
+ definition: validationResult.definition,
15674
+ payload: command.payload,
15675
+ });
15676
+ if (!applyResult.valid) {
15677
+ return appendReplayCommandContext(applyResult, {
15678
+ commandIndex: index,
15679
+ command,
15680
+ });
15681
+ }
15682
+
15683
+ workingState = applyResult.state;
15684
+ }
15685
+
15686
+ const finalStateResult = validateState({
15687
+ state: workingState,
15688
+ });
15689
+ if (!finalStateResult.valid) {
15690
+ return finalStateResult;
15575
15691
  }
15576
15692
 
15577
15693
  return {
15578
15694
  valid: true,
15579
- state: finalState,
15695
+ state: workingState,
15580
15696
  };
15581
15697
  });
15582
15698
  };