@piemekanika/x-machina 0.0.1 → 0.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.
Files changed (52) hide show
  1. package/README.md +57 -5
  2. package/dist/XMachina.d.ts +21 -0
  3. package/dist/XMachina.d.ts.map +1 -0
  4. package/dist/XMachina.js +64 -0
  5. package/dist/__tests__/XMachina.test.d.ts +2 -0
  6. package/dist/__tests__/XMachina.test.d.ts.map +1 -0
  7. package/dist/__tests__/XMachina.test.js +114 -0
  8. package/dist/helpers/__tests__/deepMerge.test.d.ts +2 -0
  9. package/dist/helpers/__tests__/deepMerge.test.d.ts.map +1 -0
  10. package/dist/helpers/__tests__/deepMerge.test.js +51 -0
  11. package/dist/helpers/__tests__/isObject.test.d.ts +2 -0
  12. package/dist/helpers/__tests__/isObject.test.d.ts.map +1 -0
  13. package/dist/helpers/__tests__/isObject.test.js +28 -0
  14. package/dist/helpers/__tests__/jsonSafe.test.d.ts +2 -0
  15. package/dist/helpers/__tests__/jsonSafe.test.d.ts.map +1 -0
  16. package/dist/helpers/__tests__/jsonSafe.test.js +138 -0
  17. package/dist/helpers/__tests__/makeReadonlyObject.test.d.ts +2 -0
  18. package/dist/helpers/__tests__/makeReadonlyObject.test.d.ts.map +1 -0
  19. package/dist/helpers/__tests__/makeReadonlyObject.test.js +149 -0
  20. package/dist/helpers/__tests__/toPromise.test.d.ts +2 -0
  21. package/dist/helpers/__tests__/toPromise.test.d.ts.map +1 -0
  22. package/dist/helpers/__tests__/toPromise.test.js +31 -0
  23. package/dist/helpers/deepMerge.d.ts +2 -0
  24. package/dist/helpers/deepMerge.d.ts.map +1 -0
  25. package/dist/helpers/deepMerge.js +20 -0
  26. package/dist/helpers/dummyClone.d.ts +11 -0
  27. package/dist/helpers/dummyClone.d.ts.map +1 -0
  28. package/dist/helpers/dummyClone.js +15 -0
  29. package/dist/helpers/isObject.d.ts +2 -0
  30. package/dist/helpers/isObject.d.ts.map +1 -0
  31. package/dist/helpers/isObject.js +3 -0
  32. package/dist/helpers/jsonSafe.d.ts +5 -0
  33. package/dist/helpers/jsonSafe.d.ts.map +1 -0
  34. package/dist/helpers/jsonSafe.js +67 -0
  35. package/dist/helpers/makeReadonlyObject.d.ts +2 -0
  36. package/dist/helpers/makeReadonlyObject.d.ts.map +1 -0
  37. package/dist/helpers/makeReadonlyObject.js +27 -0
  38. package/dist/helpers/toPromise.d.ts +2 -0
  39. package/dist/helpers/toPromise.d.ts.map +1 -0
  40. package/dist/helpers/toPromise.js +3 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +128 -0
  44. package/dist/src/XMachina.d.ts +53 -0
  45. package/dist/src/XMachina.d.ts.map +1 -0
  46. package/dist/src/XMachina.js +95 -0
  47. package/dist/types.d.ts +6 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +1 -0
  50. package/package.json +1 -1
  51. package/src/XMachina.ts +5 -5
  52. package/src/__tests__/XMachina.test.ts +61 -0
package/README.md CHANGED
@@ -47,28 +47,39 @@ const order = new XMachina<OrderContext, OrderState>({
47
47
  pending: {
48
48
  transitions: {
49
49
  pay: (ctx, paymentId) => {
50
+ // Business logic: charge customer, send receipt, etc.
50
51
  ctx.trackingNumber = `PAY-${paymentId}`
51
52
  return 'paid'
52
53
  },
53
- cancel: () => 'cancelled',
54
+ cancel: () => {
55
+ // Business logic: refund if applicable, notify customer, etc.
56
+ return 'cancelled'
57
+ },
54
58
  },
55
59
  },
56
60
  paid: {
57
61
  transitions: {
58
62
  ship: (ctx, tracking) => {
63
+ // Business logic: notify shipping carrier, update inventory, etc.
59
64
  ctx.trackingNumber = tracking
60
65
  return 'shipped'
61
66
  },
62
- cancel: () => 'cancelled',
67
+ cancel: () => {
68
+ // Business logic: process refund, restock items, etc.
69
+ return 'cancelled'
70
+ },
63
71
  },
64
72
  },
65
73
  shipped: {
66
74
  transitions: {
67
- deliver: () => 'delivered',
75
+ deliver: () => {
76
+ // Business logic: confirm delivery, update records, etc.
77
+ return 'delivered'
78
+ },
68
79
  },
69
80
  },
70
- delivered: { transitions: {} },
71
- cancelled: { transitions: {} },
81
+ delivered: null,
82
+ cancelled: null,
72
83
  },
73
84
  })
74
85
 
@@ -92,6 +103,47 @@ console.log(order.getJsonDump())
92
103
  // {"state":"delivered","context":{"orderId":"ORD-12345",...}}
93
104
  ```
94
105
 
106
+ ## API
107
+
108
+ ### Properties
109
+
110
+ | Property | Type | Description |
111
+ |----------|------|-------------|
112
+ | `state` | `S` | Current state |
113
+ | `context` | `Readonly<C>` | Readonly view of the context |
114
+ | `isFinal` | `boolean` | Whether the machine is in a final state |
115
+
116
+ ### Methods
117
+
118
+ | Method | Description |
119
+ |--------|-------------|
120
+ | `transition(event, data?)` | Trigger a transition by event name |
121
+ | `updateContext(patch)` | Merge a partial patch into the context |
122
+ | `getJsonDump()` | Serialize state and context as JSON |
123
+
124
+ ### Final States
125
+
126
+ A state set to `null` is a final state. Once reached:
127
+
128
+ - `isFinal` returns `true`
129
+ - `transition()` throws an error
130
+ - `updateContext()` throws an error
131
+
132
+ ```typescript
133
+ delivered: null, // final state
134
+ cancelled: null, // final state
135
+ ```
136
+
137
+ ### Updating Context
138
+
139
+ Use `updateContext()` to merge patches into context without direct mutation:
140
+
141
+ ```typescript
142
+ await order.transition('pay', 'txn_abc123')
143
+ order.updateContext({ lastUpdated: new Date() })
144
+ console.log(order.context.lastUpdated) // Date object
145
+ ```
146
+
95
147
  ## Development
96
148
 
97
149
  ```bash
@@ -0,0 +1,21 @@
1
+ import { MachinaContext, MachinaStates } from './types';
2
+ export type XMachinaParams<C extends MachinaContext, S extends string> = {
3
+ initialState: S;
4
+ initialContext: C;
5
+ states: MachinaStates<C, S>;
6
+ };
7
+ export declare class XMachina<C extends MachinaContext, S extends string> {
8
+ private _state;
9
+ private _context;
10
+ private states;
11
+ constructor(params: XMachinaParams<C, S>);
12
+ get state(): S;
13
+ get context(): Readonly<C>;
14
+ get isFinal(): boolean;
15
+ private getHandlerByEventName;
16
+ transition(event: string, data?: any): Promise<void>;
17
+ private goTo;
18
+ updateContext(patch: Partial<C>): void;
19
+ getJsonDump(): string;
20
+ }
21
+ //# sourceMappingURL=XMachina.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XMachina.d.ts","sourceRoot":"","sources":["../src/XMachina.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAgB,MAAM,SAAS,CAAA;AAErE,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM,IAAI;IACxE,YAAY,EAAE,CAAC,CAAA;IACf,cAAc,EAAE,CAAC,CAAA;IACjB,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CAC3B,CAAA;AAED,qBAAa,QAAQ,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM;IAC/D,OAAO,CAAC,MAAM,CAAG;IACjB,OAAO,CAAC,QAAQ,CAAG;IACnB,OAAO,CAAC,MAAM,CAAqB;IAEnC,YAAY,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAMvC;IAED,IAAI,KAAK,IAAI,CAAC,CAEb;IAED,IAAI,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,CAEzB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,OAAO,CAAC,qBAAqB;IAoBvB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAazD;YAEa,IAAI;IAQlB,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAQrC;IAED,WAAW,IAAI,MAAM,CAKpB;CACD"}
@@ -0,0 +1,64 @@
1
+ import { deepMerge } from './helpers/deepMerge';
2
+ import { toPromise } from './helpers/toPromise';
3
+ import { makeReadonlyObject } from './helpers/makeReadonlyObject';
4
+ import { dummyClone } from './helpers/dummyClone';
5
+ export class XMachina {
6
+ _state;
7
+ _context;
8
+ states;
9
+ constructor(params) {
10
+ this._state = params.initialState;
11
+ this._context = dummyClone(params.initialContext);
12
+ this.states = params.states;
13
+ return this;
14
+ }
15
+ get state() {
16
+ return this._state;
17
+ }
18
+ get context() {
19
+ return makeReadonlyObject(this._context);
20
+ }
21
+ get isFinal() {
22
+ return this.states[this._state] === null;
23
+ }
24
+ getHandlerByEventName(event) {
25
+ const stateDefinition = this.states[this.state];
26
+ if (!stateDefinition) {
27
+ throw new Error(`No transition handler for event '${event}' in state '${this.state}'`);
28
+ }
29
+ const transitions = stateDefinition.transitions;
30
+ const handler = transitions?.[event];
31
+ if (!handler) {
32
+ throw new Error(`No transition handler for event '${event}' in state '${this.state}'`);
33
+ }
34
+ return handler;
35
+ }
36
+ async transition(event, data) {
37
+ if (this.isFinal) {
38
+ throw new Error(`Cannot transition from final state '${this._state}'`);
39
+ }
40
+ const handler = this.getHandlerByEventName(event);
41
+ const newState = await toPromise(() => handler(this._context, data))();
42
+ if (newState) {
43
+ await this.goTo(newState);
44
+ }
45
+ }
46
+ async goTo(newState) {
47
+ if (!(newState in this.states)) {
48
+ throw new Error(`State '${newState}' is not defined`);
49
+ }
50
+ this._state = newState;
51
+ }
52
+ updateContext(patch) {
53
+ if (this.isFinal) {
54
+ throw new Error(`Cannot update context in final state '${this._state}'`);
55
+ }
56
+ this._context = deepMerge(this._context, patch);
57
+ }
58
+ getJsonDump() {
59
+ return JSON.stringify({
60
+ state: this.state,
61
+ context: this.context,
62
+ });
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=XMachina.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XMachina.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/XMachina.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { XMachina } from '../XMachina';
3
+ describe('XMachina', () => {
4
+ let machine;
5
+ beforeEach(() => {
6
+ machine = new XMachina('idle', { count: 0 }, {
7
+ idle: {
8
+ transitions: {
9
+ start: (ctx) => {
10
+ ctx.count = 1;
11
+ return 'active';
12
+ },
13
+ },
14
+ },
15
+ active: {
16
+ transitions: {
17
+ finish: () => 'finished',
18
+ increment: (ctx) => {
19
+ ctx.count += 1;
20
+ return null;
21
+ },
22
+ setUser: (ctx, data) => {
23
+ ctx.user = data;
24
+ return null;
25
+ },
26
+ },
27
+ },
28
+ finished: {
29
+ transitions: {},
30
+ },
31
+ });
32
+ });
33
+ describe('constructor', () => {
34
+ it('sets initial state', () => {
35
+ expect(machine.state).toBe('idle');
36
+ });
37
+ it('sets initial context', () => {
38
+ expect(machine.context).toEqual({ count: 0 });
39
+ });
40
+ it('does not mutate original context object', () => {
41
+ const originalContext = { count: 0 };
42
+ const m = new XMachina('idle', originalContext, { idle: {} });
43
+ m.updateContext({ count: 5 });
44
+ expect(originalContext).toEqual({ count: 0 });
45
+ });
46
+ });
47
+ describe('state getter', () => {
48
+ it('returns current state', () => {
49
+ expect(machine.state).toBe('idle');
50
+ });
51
+ });
52
+ describe('context getter', () => {
53
+ it('returns readonly context', () => {
54
+ const context = machine.context;
55
+ expect(context).toEqual({ count: 0 });
56
+ });
57
+ it('returns a frozen copy of the context', () => {
58
+ const context = machine.context;
59
+ expect(() => (context.count = 999)).toThrow();
60
+ expect(machine.context).toEqual({ count: 0 });
61
+ });
62
+ });
63
+ describe('transition', () => {
64
+ it('changes state when transition handler returns new state', async () => {
65
+ await machine.transition('start');
66
+ expect(machine.state).toBe('active');
67
+ });
68
+ it('updates context when handler modifies it', async () => {
69
+ await machine.transition('start');
70
+ expect(machine.context.count).toBe(1);
71
+ });
72
+ it('stays in same state when handler returns null', async () => {
73
+ await machine.transition('start');
74
+ await machine.transition('increment');
75
+ expect(machine.state).toBe('active');
76
+ expect(machine.context.count).toBe(2);
77
+ });
78
+ it('can pass data to transition handler', async () => {
79
+ await machine.transition('start');
80
+ await machine.transition('setUser', { name: 'Alice' });
81
+ expect(machine.context.user).toEqual({ name: 'Alice' });
82
+ });
83
+ it('throws when no transition handler for event in current state', async () => {
84
+ await expect(machine.transition('finish')).rejects.toThrow("No transition handler for event 'finish' in state 'idle'");
85
+ });
86
+ it('throws when transition returns undefined state', async () => {
87
+ const machineWithBadTransition = new XMachina('idle', { count: 0 }, {
88
+ idle: {
89
+ transitions: {
90
+ goToUnknown: () => 'unknown',
91
+ },
92
+ },
93
+ active: { transitions: {} },
94
+ finished: { transitions: {} },
95
+ });
96
+ await expect(machineWithBadTransition.transition('goToUnknown')).rejects.toThrow("State 'unknown' is not defined");
97
+ });
98
+ });
99
+ describe('updateContext', () => {
100
+ it('merges partial updates into context', () => {
101
+ machine.updateContext({ count: 10 });
102
+ expect(machine.context).toEqual({ count: 10 });
103
+ });
104
+ it('deep merges nested objects', () => {
105
+ machine.updateContext({ user: { name: 'Bob' } });
106
+ expect(machine.context).toEqual({ count: 0, user: { name: 'Bob' } });
107
+ });
108
+ it('preserves existing properties not being updated', () => {
109
+ machine.updateContext({ count: 5 });
110
+ expect(machine.context.count).toBe(5);
111
+ expect(machine.context.count).toBeDefined();
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=deepMerge.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deepMerge.test.d.ts","sourceRoot":"","sources":["../../../src/helpers/__tests__/deepMerge.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { deepMerge } from '../deepMerge';
3
+ describe('deepMerge', () => {
4
+ it('merges two flat objects', () => {
5
+ const target = { a: 1, b: 2 };
6
+ const source = { b: 3, c: 4 };
7
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: 3, c: 4 });
8
+ });
9
+ it('does not mutate original objects', () => {
10
+ const target = { a: 1 };
11
+ const source = { b: 2 };
12
+ deepMerge(target, source);
13
+ expect(target).toEqual({ a: 1 });
14
+ expect(source).toEqual({ b: 2 });
15
+ });
16
+ it('deeply merges nested objects', () => {
17
+ const target = { a: { b: 1, c: 2 } };
18
+ const source = { a: { b: 3, d: 4 } };
19
+ expect(deepMerge(target, source)).toEqual({ a: { b: 3, c: 2, d: 4 } });
20
+ });
21
+ it('adds new nested objects from source', () => {
22
+ const target = { a: 1 };
23
+ const source = { b: { nested: true } };
24
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: { nested: true } });
25
+ });
26
+ it('overwrites primitives with source values', () => {
27
+ const target = { a: 1, b: 'old' };
28
+ const source = { b: 'new', c: true };
29
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: 'new', c: true });
30
+ });
31
+ it('handles empty objects', () => {
32
+ expect(deepMerge({}, {})).toEqual({});
33
+ expect(deepMerge({ a: 1 }, {})).toEqual({ a: 1 });
34
+ expect(deepMerge({}, { b: 2 })).toEqual({ b: 2 });
35
+ });
36
+ it('handles arrays - source replaces target array', () => {
37
+ const target = { arr: [1, 2, 3] };
38
+ const source = { arr: [4, 5] };
39
+ expect(deepMerge(target, source)).toEqual({ arr: [4, 5] });
40
+ });
41
+ it('handles null values in source', () => {
42
+ const target = { a: 1 };
43
+ const source = { a: null };
44
+ expect(deepMerge(target, source)).toEqual({ a: null });
45
+ });
46
+ it('handles undefined values in source', () => {
47
+ const target = { a: 1 };
48
+ const source = { a: undefined };
49
+ expect(deepMerge(target, source)).toEqual({ a: undefined });
50
+ });
51
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=isObject.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isObject.test.d.ts","sourceRoot":"","sources":["../../../src/helpers/__tests__/isObject.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isObject } from '../isObject';
3
+ describe('isObject', () => {
4
+ it('returns true for plain objects', () => {
5
+ expect(isObject({})).toBe(true);
6
+ expect(isObject({ a: 1 })).toBe(true);
7
+ expect(isObject({ nested: { deep: true } })).toBe(true);
8
+ });
9
+ it('returns false for arrays', () => {
10
+ expect(isObject([])).toBe(false);
11
+ expect(isObject([1, 2, 3])).toBe(false);
12
+ expect(isObject([{}, {}])).toBe(false);
13
+ });
14
+ it('returns false for null', () => {
15
+ expect(isObject(null)).toBe(false);
16
+ });
17
+ it('returns false for primitives', () => {
18
+ expect(isObject('string')).toBe(false);
19
+ expect(isObject(123)).toBe(false);
20
+ expect(isObject(true)).toBe(false);
21
+ expect(isObject(undefined)).toBe(false);
22
+ expect(isObject(Symbol('test'))).toBe(false);
23
+ });
24
+ it('returns false for functions', () => {
25
+ expect(isObject(() => { })).toBe(false);
26
+ expect(isObject(function () { })).toBe(false);
27
+ });
28
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jsonSafe.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonSafe.test.d.ts","sourceRoot":"","sources":["../../../src/helpers/__tests__/jsonSafe.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isJsonSafe, assertJsonSafe, toJson, fromJson } from '../jsonSafe';
3
+ describe('isJsonSafe', () => {
4
+ it('returns true for primitives', () => {
5
+ expect(isJsonSafe(null)).toBe(true);
6
+ expect(isJsonSafe(true)).toBe(true);
7
+ expect(isJsonSafe(false)).toBe(true);
8
+ expect(isJsonSafe(0)).toBe(true);
9
+ expect(isJsonSafe(42)).toBe(true);
10
+ expect(isJsonSafe(-3.14)).toBe(true);
11
+ expect(isJsonSafe('')).toBe(true);
12
+ expect(isJsonSafe('hello')).toBe(true);
13
+ expect(isJsonSafe('hello world')).toBe(true);
14
+ });
15
+ it('returns true for plain objects', () => {
16
+ expect(isJsonSafe({})).toBe(true);
17
+ expect(isJsonSafe({ a: 1 })).toBe(true);
18
+ expect(isJsonSafe({ nested: { deep: true } })).toBe(true);
19
+ expect(isJsonSafe({ arr: [1, 2, 3] })).toBe(true);
20
+ });
21
+ it('returns true for arrays', () => {
22
+ expect(isJsonSafe([])).toBe(true);
23
+ expect(isJsonSafe([1, 2, 3])).toBe(true);
24
+ expect(isJsonSafe([{}, [], 'string'])).toBe(true);
25
+ expect(isJsonSafe([[1, 2], [3, 4]])).toBe(true);
26
+ });
27
+ it('returns true for Date', () => {
28
+ expect(isJsonSafe(new Date())).toBe(true);
29
+ });
30
+ it('returns true for RegExp', () => {
31
+ expect(isJsonSafe(/test/)).toBe(true);
32
+ expect(isJsonSafe(new RegExp('pattern'))).toBe(true);
33
+ });
34
+ it('returns false for functions', () => {
35
+ expect(isJsonSafe(() => { })).toBe(false);
36
+ expect(isJsonSafe(function () { })).toBe(false);
37
+ expect(isJsonSafe(Math.random)).toBe(false);
38
+ });
39
+ it('returns false for undefined', () => {
40
+ expect(isJsonSafe(undefined)).toBe(false);
41
+ });
42
+ it('returns false for symbols', () => {
43
+ expect(isJsonSafe(Symbol('test'))).toBe(false);
44
+ });
45
+ it('returns false for bigint', () => {
46
+ expect(isJsonSafe(BigInt(123))).toBe(false);
47
+ });
48
+ it('returns false for Map', () => {
49
+ expect(isJsonSafe(new Map())).toBe(false);
50
+ expect(isJsonSafe(new Map([['a', 1]]))).toBe(false);
51
+ });
52
+ it('returns false for Set', () => {
53
+ expect(isJsonSafe(new Set())).toBe(false);
54
+ expect(isJsonSafe(new Set([1, 2, 3]))).toBe(false);
55
+ });
56
+ it('returns false for Error', () => {
57
+ expect(isJsonSafe(new Error())).toBe(false);
58
+ expect(isJsonSafe(new TypeError('msg'))).toBe(false);
59
+ });
60
+ it('returns false for objects with unsafe values', () => {
61
+ expect(isJsonSafe({ fn: () => { } })).toBe(false);
62
+ expect(isJsonSafe({ u: undefined })).toBe(false);
63
+ expect(isJsonSafe([() => { }])).toBe(false);
64
+ });
65
+ it('returns true for deeply nested safe objects', () => {
66
+ expect(isJsonSafe({ a: { b: { c: { d: { e: 1 } } } } })).toBe(true);
67
+ expect(isJsonSafe({ arr: [{ nested: { arr: [1, 2, { deep: true }] } }] })).toBe(true);
68
+ });
69
+ });
70
+ describe('assertJsonSafe', () => {
71
+ it('does not throw for safe values', () => {
72
+ expect(() => assertJsonSafe(null)).not.toThrow();
73
+ expect(() => assertJsonSafe({})).not.toThrow();
74
+ expect(() => assertJsonSafe({ a: 1 })).not.toThrow();
75
+ expect(() => assertJsonSafe([1, 2, 3])).not.toThrow();
76
+ expect(() => assertJsonSafe(new Date())).not.toThrow();
77
+ expect(() => assertJsonSafe({ nested: { deep: true } })).not.toThrow();
78
+ });
79
+ it('throws with path for function at root', () => {
80
+ expect(() => assertJsonSafe((() => { }))).toThrow('Function at root is not JSON-safe');
81
+ });
82
+ it('throws with path for function in object', () => {
83
+ expect(() => assertJsonSafe({ fn: (() => { }) })).toThrow('Function at fn is not JSON-safe');
84
+ });
85
+ it('throws with path for undefined at root', () => {
86
+ expect(() => assertJsonSafe(undefined)).toThrow('Undefined at root is not JSON-safe');
87
+ });
88
+ it('throws with path for undefined in array', () => {
89
+ expect(() => assertJsonSafe([1, undefined, 3])).toThrow('Undefined at [1] is not JSON-safe');
90
+ });
91
+ it('throws with path for Map', () => {
92
+ expect(() => assertJsonSafe(new Map())).toThrow('Map at is not JSON-safe');
93
+ });
94
+ it('throws with path for Set', () => {
95
+ expect(() => assertJsonSafe(new Set())).toThrow('Set at is not JSON-safe');
96
+ });
97
+ it('throws with path for Error', () => {
98
+ expect(() => assertJsonSafe(new Error())).toThrow('Error at is not JSON-safe');
99
+ });
100
+ it('includes nested paths', () => {
101
+ expect(() => assertJsonSafe({ a: { b: { c: { d: { e: undefined } } } } }))
102
+ .toThrow('a.b.c.d.e');
103
+ });
104
+ });
105
+ describe('toJson', () => {
106
+ it('serializes safe values to JSON string', () => {
107
+ expect(toJson(null)).toBe('null');
108
+ expect(toJson(true)).toBe('true');
109
+ expect(toJson(42)).toBe('42');
110
+ expect(toJson('hello')).toBe('"hello"');
111
+ expect(toJson({ a: 1 })).toBe('{"a":1}');
112
+ expect(toJson([1, 2, 3])).toBe('[1,2,3]');
113
+ });
114
+ it('serializes nested objects', () => {
115
+ const obj = { a: { b: { c: 1 } }, arr: [1, 2, { nested: true }] };
116
+ expect(toJson(obj)).toBe('{"a":{"b":{"c":1}},"arr":[1,2,{"nested":true}]}');
117
+ });
118
+ });
119
+ describe('fromJson', () => {
120
+ it('parses JSON string to object', () => {
121
+ expect(fromJson('null')).toBe(null);
122
+ expect(fromJson('true')).toBe(true);
123
+ expect(fromJson('42')).toBe(42);
124
+ expect(fromJson('"hello"')).toBe('hello');
125
+ expect(fromJson('{"a":1}')).toEqual({ a: 1 });
126
+ expect(fromJson('[1,2,3]')).toEqual([1, 2, 3]);
127
+ });
128
+ it('parses nested objects', () => {
129
+ const json = '{"a":{"b":{"c":1}},"arr":[1,2,{"nested":true}]}';
130
+ expect(fromJson(json)).toEqual({ a: { b: { c: 1 } }, arr: [1, 2, { nested: true }] });
131
+ });
132
+ it('round-trips safely through toJson and fromJson', () => {
133
+ const original = { a: 1, b: { c: 2 }, arr: [1, 2, 3], nested: { deep: { value: 'test' } } };
134
+ const json = toJson(original);
135
+ const restored = fromJson(json);
136
+ expect(restored).toEqual(original);
137
+ });
138
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=makeReadonlyObject.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"makeReadonlyObject.test.d.ts","sourceRoot":"","sources":["../../../src/helpers/__tests__/makeReadonlyObject.test.ts"],"names":[],"mappings":""}