@routevn/creator-model 1.2.6 → 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.6",
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
@@ -3298,12 +3298,13 @@ const validateLayoutElementData = ({
3298
3298
 
3299
3299
  if (
3300
3300
  data.direction !== undefined &&
3301
+ data.direction !== "absolute" &&
3301
3302
  data.direction !== "horizontal" &&
3302
3303
  data.direction !== "vertical"
3303
3304
  ) {
3304
3305
  return invalidFromErrorFactory(
3305
3306
  errorFactory,
3306
- `${path}.direction must be 'horizontal' or 'vertical' when provided`,
3307
+ `${path}.direction must be 'absolute', 'horizontal' or 'vertical' when provided`,
3307
3308
  );
3308
3309
  }
3309
3310
 
@@ -15477,50 +15478,118 @@ export const validatePayload = ({ type, payload }) => {
15477
15478
  });
15478
15479
  };
15479
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
+
15480
15575
  export const validateAgainstState = ({ state, command }) => {
15481
15576
  return captureValidation(() => {
15482
15577
  const normalizedState = normalizeStateCollections(state);
15483
15578
 
15484
- if (!isPlainObject(command)) {
15485
- return invalidPrecondition("command must be an object");
15486
- }
15487
-
15488
- const payloadResult = validatePayload(command);
15489
- if (!payloadResult.valid) {
15490
- return invalidPayload(
15491
- payloadResult.error.message,
15492
- toDomainErrorDetails(payloadResult.error),
15493
- );
15494
- }
15495
-
15496
15579
  const stateResult = validateState({ state: normalizedState });
15497
15580
  if (!stateResult.valid) {
15498
- if (stateResult.error.kind === "invariant") {
15499
- return invalidInvariant(
15500
- stateResult.error.message,
15501
- toDomainErrorDetails(stateResult.error),
15502
- );
15503
- }
15504
-
15505
- return invalidState(
15506
- stateResult.error.message,
15507
- toDomainErrorDetails(stateResult.error),
15508
- );
15581
+ return normalizeCurrentStateValidationResult(stateResult);
15509
15582
  }
15510
15583
 
15511
- const definition = getCommandDefinition({ type: command.type });
15512
- if (!definition) {
15513
- return invalidPrecondition(`unknown command type '${command.type}'`);
15584
+ const validationResult = validateCommandDefinitionAgainstState({
15585
+ state: normalizedState,
15586
+ command,
15587
+ });
15588
+ if (!validationResult.valid) {
15589
+ return validationResult;
15514
15590
  }
15515
15591
 
15516
- const validationResult = captureValidation(() =>
15517
- definition.validateAgainstState({
15518
- state: normalizedState,
15519
- payload: command.payload,
15520
- }),
15521
- );
15522
-
15523
- return normalizeStateResult(validationResult);
15592
+ return VALID_RESULT;
15524
15593
  });
15525
15594
  };
15526
15595
 
@@ -15533,49 +15602,97 @@ export const processCommand = ({ state, command }) => {
15533
15602
  command.type.startsWith("spritesheet.") ||
15534
15603
  command.type.startsWith("particle."));
15535
15604
 
15536
- if (!isPlainObject(command)) {
15537
- return invalidPrecondition("command must be an object");
15605
+ const stateResult = validateState({ state: normalizedState });
15606
+ if (!stateResult.valid) {
15607
+ return normalizeCurrentStateValidationResult(stateResult);
15538
15608
  }
15539
15609
 
15540
- const preconditionResult = validateAgainstState({
15610
+ const validationResult = validateCommandDefinitionAgainstState({
15541
15611
  state: normalizedState,
15542
15612
  command,
15543
15613
  });
15544
- if (!preconditionResult.valid) {
15545
- return preconditionResult;
15614
+ if (!validationResult.valid) {
15615
+ return validationResult;
15546
15616
  }
15547
15617
 
15548
- const definition = getCommandDefinition({ type: command.type });
15549
- if (!definition) {
15550
- return invalidPrecondition(`unknown command type '${command.type}'`);
15551
- }
15552
-
15553
- const nextState = definition.reduce({
15618
+ const applyResult = applyCommandDefinition({
15554
15619
  state: structuredClone(
15555
15620
  shouldMaterializeNormalizedState ? normalizedState : state,
15556
15621
  ),
15622
+ definition: validationResult.definition,
15557
15623
  payload: command.payload,
15558
15624
  });
15559
- if (nextState?.valid === false) {
15560
- 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;
15561
15634
  }
15562
15635
 
15563
- const finalState =
15564
- nextState === undefined
15565
- ? shouldMaterializeNormalizedState
15566
- ? normalizedState
15567
- : state
15568
- : 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);
15569
15650
  const stateResult = validateState({
15570
- state: finalState,
15651
+ state: normalizedState,
15571
15652
  });
15572
15653
  if (!stateResult.valid) {
15573
- 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;
15574
15691
  }
15575
15692
 
15576
15693
  return {
15577
15694
  valid: true,
15578
- state: finalState,
15695
+ state: workingState,
15579
15696
  };
15580
15697
  });
15581
15698
  };