Skip to content

Commit 3e98696

Browse files
authored
Remove Redux Toolkit from @trrack/core and expand core coverage (#76)
This PR removes the Redux Toolkit dependency from @trrack/core and replaces the small amount of RTK functionality the package was using with local implementations. It also fixes a couple of core typing/runtime issues uncovered during the refactor and significantly expands the packages/core test suite. What changed - Removed @reduxjs/toolkit from packages/core - Added a local action module for: - PayloadAction - PayloadActionCreator - createAction - Replaced the RTK-backed provenance graph store/slice/listener setup with a local reducer/action implementation - Updated generated dist artifacts and package metadata - Updated the dummy app to import PayloadAction from @trrack/core - Updated the createAction API doc to reflect the local implementation - Follow-up fixes included - Fixed Registry.register typing so normal mutating state reducers type-check correctly - Fixed StateChangeFunction typing to allow Immer-style reducers that mutate state and return void - Fixed checkpoint vs patch detection in trrack so multi-key updates are no longer misclassified as patches - Tests Expanded packages/core coverage with new and improved tests for: - node construction - registry/action creator behavior - current-change listeners - traversal across branches - side-effect replay during traversal - import/export fidelity on branched graphs - patch vs checkpoint storage - event listener unsubscribe behavior Compatibility notes - For standard initializeTrrack usage, behavior should remain effectively the same. Potential compatibility risks: - createAction(type, prepareAction) is no longer supported - initializeProvenanceGraph().update is no longer Redux-dispatch-like; it now accepts Trrack graph actions directly - exported graph payloads may differ because some states now correctly store as checkpoints instead of patches
2 parents bdda3b8 + b412e52 commit 3e98696

16 files changed

Lines changed: 555 additions & 112 deletions

File tree

apps/docs/pages/api-reference/functions/createAction.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ allowing it to be used in reducer logic that is looking for that action type.
2929

3030
#### Defined in
3131

32-
node_modules/@reduxjs/toolkit/dist/createAction.d.ts:163
32+
[action.ts](../../../../../packages/core/src/action.ts#L155)
3333

3434
**createAction**\<`PA`, `T`\>(`type`, `prepareAction`): `PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
3535

36-
A utility function to create an action creator for the given action type
37-
string. The action creator accepts a single argument, which will be included
38-
in the action object as a field called payload. The action creator function
39-
will also have its toString() overridden so that it returns the action type,
36+
A utility function to create an action creator for the given action type
37+
string. The action creator's parameters mirror the `prepareAction`
38+
function signature, and any arguments passed to the action creator are
39+
forwarded to `prepareAction`. The resulting payload is included in the
40+
action object as a field called `payload`. The action creator function will
41+
also have its toString() overridden so that it returns the action type,
4042
allowing it to be used in reducer logic that is looking for that action type.
4143

4244
#### Type parameters
@@ -59,4 +61,4 @@ allowing it to be used in reducer logic that is looking for that action type.
5961

6062
#### Defined in
6163

62-
node_modules/@reduxjs/toolkit/dist/createAction.d.ts:177
64+
[action.ts](../../../../../packages/core/src/action.ts#L158)

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"url": "https://github.com/Trrack/trrackjs.git"
2222
},
2323
"dependencies": {
24-
"@reduxjs/toolkit": "^1.9.1",
2524
"fast-json-patch": "^3.1.1",
25+
"immer": "^9.0.21",
2626
"uuid": "^11.0.0"
2727
},
2828
"devDependencies": {

packages/core/src/action.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, expectTypeOf, it } from 'vitest';
2+
3+
import { createAction } from './action';
4+
5+
describe('createAction', () => {
6+
it('supports prepared action creators with multiple args and meta/error', () => {
7+
const setRange = createAction(
8+
'set-range',
9+
(start: number, end: number) => ({
10+
payload: { start, end },
11+
meta: { source: 'slider' as const },
12+
error: false as const,
13+
})
14+
);
15+
16+
const action = setRange(1, 3);
17+
18+
expectTypeOf(action.payload).toEqualTypeOf<{
19+
start: number;
20+
end: number;
21+
}>();
22+
expectTypeOf(action.meta).toEqualTypeOf<{
23+
source: 'slider';
24+
}>();
25+
expectTypeOf(action.error).toEqualTypeOf<false>();
26+
expect(action).toEqual({
27+
payload: { start: 1, end: 3 },
28+
meta: { source: 'slider' },
29+
error: false,
30+
type: 'set-range',
31+
});
32+
});
33+
34+
it('returns false from match for nullish and non-matching inputs', () => {
35+
const mark = createAction<string>('mark');
36+
37+
expect(mark.match(null)).toBe(false);
38+
expect(mark.match(undefined)).toBe(false);
39+
expect(mark.match({ type: 'other' })).toBe(false);
40+
expect(mark.match(mark('A'))).toBe(true);
41+
});
42+
});

packages/core/src/action.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
export type PayloadAction<
3+
Payload = void,
4+
Type extends string = string,
5+
Meta = never,
6+
Error = never
7+
> = {
8+
payload: Payload;
9+
type: Type;
10+
} & ([Meta] extends [never] ? unknown : { meta: Meta })
11+
& ([Error] extends [never] ? unknown : { error: Error });
12+
13+
type ActionLike = {
14+
type: string;
15+
};
16+
17+
export type PrepareAction<Payload> =
18+
| ((...args: any[]) => { payload: Payload })
19+
| ((...args: any[]) => { payload: Payload; meta: any })
20+
| ((...args: any[]) => { payload: Payload; error: any })
21+
| ((...args: any[]) => { payload: Payload; meta: any; error: any });
22+
23+
type IsAny<T, True, False = never> = true | false extends (
24+
T extends never ? true : false
25+
)
26+
? True
27+
: False;
28+
29+
type IsUnknown<T, True, False = never> = unknown extends T
30+
? IsAny<T, False, True>
31+
: False;
32+
33+
type IfMaybeUndefined<Payload, True, False> = [undefined] extends [Payload]
34+
? True
35+
: False;
36+
37+
type IfVoid<Payload, True, False> = [void] extends [Payload] ? True : False;
38+
39+
type IsEmptyObject<T, True, False = never> = T extends unknown
40+
? keyof T extends never
41+
? IsUnknown<T, False, IfMaybeUndefined<T, False, IfVoid<T, False, True>>>
42+
: False
43+
: never;
44+
45+
type AtLeastTs35<True, False> = [True, False][IsUnknown<
46+
ReturnType<(<T>() => T)>,
47+
0,
48+
1
49+
>];
50+
51+
type IsUnknownOrNonInferrable<T, True, False> = AtLeastTs35<
52+
IsUnknown<T, True, False>,
53+
IsEmptyObject<T, True, IsUnknown<T, True, False>>
54+
>;
55+
56+
type BaseActionCreator<
57+
Payload,
58+
Type extends string,
59+
Meta = never,
60+
Error = never
61+
> = {
62+
type: Type;
63+
toString(): Type;
64+
match(action: unknown): action is PayloadAction<Payload, Type, Meta, Error>;
65+
};
66+
67+
export type ActionCreatorWithPayload<
68+
Payload,
69+
Type extends string = string
70+
> = BaseActionCreator<Payload, Type> & {
71+
(payload: Payload): PayloadAction<Payload, Type>;
72+
};
73+
74+
export type ActionCreatorWithOptionalPayload<
75+
Payload,
76+
Type extends string = string
77+
> = BaseActionCreator<Payload, Type> & {
78+
(payload?: Payload): PayloadAction<Payload, Type>;
79+
};
80+
81+
export type ActionCreatorWithoutPayload<
82+
Type extends string = string
83+
> = BaseActionCreator<undefined, Type> & {
84+
(noArgument: void): PayloadAction<undefined, Type>;
85+
};
86+
87+
export type ActionCreatorWithNonInferrablePayload<
88+
Type extends string = string
89+
> = BaseActionCreator<unknown, Type> & {
90+
<Payload>(payload: Payload): PayloadAction<Payload, Type>;
91+
};
92+
93+
export type ActionCreatorWithPreparedPayload<
94+
Args extends unknown[],
95+
Payload,
96+
Type extends string = string,
97+
Error = never,
98+
Meta = never
99+
> = BaseActionCreator<Payload, Type, Meta, Error> & {
100+
(...args: Args): PayloadAction<Payload, Type, Meta, Error>;
101+
};
102+
103+
type PreparedPayloadActionCreator<
104+
Prepare extends PrepareAction<any>,
105+
Type extends string
106+
> = ActionCreatorWithPreparedPayload<
107+
Parameters<Prepare>,
108+
ReturnType<Prepare>['payload'],
109+
Type,
110+
ReturnType<Prepare> extends { error: infer Error } ? Error : never,
111+
ReturnType<Prepare> extends { meta: infer Meta } ? Meta : never
112+
>;
113+
114+
type _ActionCreatorWithPreparedPayload<
115+
Prepare extends PrepareAction<any> | void,
116+
Type extends string = string
117+
> = Prepare extends PrepareAction<any>
118+
? PreparedPayloadActionCreator<Prepare, Type>
119+
: void;
120+
121+
type IfPrepareActionMethodProvided<Prepare, True, False> = Prepare extends (
122+
...args: any[]
123+
) => any
124+
? True
125+
: False;
126+
127+
export type PayloadActionCreator<
128+
Payload = void,
129+
Type extends string = string,
130+
Prepare extends PrepareAction<Payload> | void = void
131+
> = IfPrepareActionMethodProvided<
132+
Prepare,
133+
_ActionCreatorWithPreparedPayload<Prepare, Type>,
134+
IsAny<
135+
Payload,
136+
// `any` must stay explicit here to preserve RTK-compatible payload behavior.
137+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
138+
ActionCreatorWithPayload<any, Type>,
139+
IsUnknownOrNonInferrable<
140+
Payload,
141+
ActionCreatorWithNonInferrablePayload<Type>,
142+
IfVoid<
143+
Payload,
144+
ActionCreatorWithoutPayload<Type>,
145+
IfMaybeUndefined<
146+
Payload,
147+
ActionCreatorWithOptionalPayload<Payload, Type>,
148+
ActionCreatorWithPayload<Payload, Type>
149+
>
150+
>
151+
>
152+
>
153+
>;
154+
155+
export function createAction<Payload = void, Type extends string = string>(
156+
type: Type
157+
): PayloadActionCreator<Payload, Type>;
158+
export function createAction<
159+
Prepare extends PrepareAction<any>,
160+
Type extends string = string
161+
>(
162+
type: Type,
163+
prepareAction: Prepare
164+
): PayloadActionCreator<ReturnType<Prepare>['payload'], Type, Prepare>;
165+
export function createAction(type: string, prepareAction?: PrepareAction<any>) {
166+
const actionCreator = ((...args: unknown[]) => {
167+
if (prepareAction) {
168+
return {
169+
...prepareAction(...args),
170+
type,
171+
};
172+
}
173+
174+
return {
175+
payload: args[0],
176+
type,
177+
};
178+
}) as PayloadActionCreator<unknown, string>;
179+
180+
actionCreator.type = type;
181+
actionCreator.toString = () => type;
182+
actionCreator.match = (
183+
action: unknown
184+
): action is PayloadAction<unknown, string> =>
185+
typeof action === 'object'
186+
&& action !== null
187+
&& 'type' in action
188+
&& (action as ActionLike).type === type;
189+
190+
return actionCreator;
191+
}

packages/core/src/graph/components/node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { PayloadAction } from '@reduxjs/toolkit';
32
import { Operation } from 'fast-json-patch';
3+
import { PayloadAction } from '../../action';
44

55
import { FlavoredId, ID } from '../../utils';
66

0 commit comments

Comments
 (0)