Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3770abc
Measure and optimize response conversion with regression coverage
zth Sep 21, 2026
883d7d4
Audit conversion semantics and compare string-path optimization
zth Sep 21, 2026
f623c59
Prepare versioned conversion plans once with independent contract tests
zth Sep 21, 2026
9cdd68b
Generate and validate prepared conversion plans end to end
zth Sep 21, 2026
e856b90
Record balanced conversion benchmarks and finalize validation
zth Sep 21, 2026
6bdf661
Reduce generated bundle overhead and enforce conversion size budgets
zth Sep 22, 2026
f366d16
Document complete bundle measurements and performance recheck
zth Sep 22, 2026
3a55220
Shrink generated conversion instructions with local callback IDs
zth Sep 22, 2026
b0f009a
Drop redundant native-list plans and retain required conversion bound…
zth Sep 22, 2026
66f95a1
Prototype ReScript conversion runtime behind isolated benchmark harness
zth Sep 22, 2026
3de062b
Record runtime port tradeoffs and retain faster shipping JavaScript
zth Sep 22, 2026
c037d41
Specialize generic records and opaque lists in prepared conversion
zth Sep 22, 2026
e1b12c7
Validate JavaScript fast paths and record paired performance measurem…
zth Sep 22, 2026
86d3049
Record final performance comparison against current default branch
zth Sep 22, 2026
64c0934
Cover plural custom scalar and catch conversions through mounted Rela…
zth Sep 22, 2026
65d953f
Exercise caught plural nullable, union and interface payloads
zth Sep 22, 2026
9adbc38
Update compiler pin with regenerated ReScript test artifacts
zth Sep 22, 2026
e30347b
Build Linux PPX on supported Debian Bookworm
zth Sep 23, 2026
721d788
Install GPG for OCaml setup in Linux PPX containers
zth Sep 23, 2026
f1d667c
Install complete GnuPG tools for OCaml signature verification
zth Sep 23, 2026
3b05c7a
Audit custom scalar ordering with permutation and mounted regressions
zth Sep 23, 2026
8cb7b98
Remove unshipped runtime prototype from merge scope
zth Sep 23, 2026
de842ba
Remove redundant release coordination note from changelog
zth Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/bindings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,17 @@ jobs:
- name: Test
run: |
yarn test:ci
ENABLE_PERSISTING=true yarn test:ci
yarn test:conversion
yarn test:conversion:mutations
- name: Generated conversion size budget
run: yarn size:conversion --check --output conversion-size.json
- name: Conversion benchmark (informational)
run: yarn bench:conversion:prepared --samples 7 --duration 20 --output conversion-benchmark.json
- uses: actions/upload-artifact@v4
with:
name: conversion-performance
path: |
packages/rescript-relay/conversion-benchmark.json
packages/rescript-relay/conversion-size.json
packages/rescript-relay/coverage/conversion/
5 changes: 3 additions & 2 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
name: Build PPX for release
runs-on: ${{ matrix.platform }}
container:
image: debian:bullseye
image: debian:bookworm
defaults:
run:
working-directory: packages/rescript-relay/rescript-relay-ppx
Expand All @@ -34,6 +34,7 @@ jobs:
curl \
ca-certificates \
git \
gnupg \
unzip \
bubblewrap
- uses: actions/checkout@v4
Expand All @@ -47,7 +48,7 @@ jobs:
with:
ocaml-compiler: "4.14"
dune-cache: true
cache-prefix: "v2-bullseye"
cache-prefix: "v3-bookworm"
- name: Install PPX dependencies
run: opam install . --deps-only
- name: Build PPX
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ppx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
name: PPX Linux
runs-on: ubuntu-latest
container:
image: debian:bullseye
image: debian:bookworm
steps:
- name: Install system dependencies
run: |
Expand All @@ -21,6 +21,7 @@ jobs:
curl \
ca-certificates \
git \
gnupg \
unzip \
bubblewrap
- uses: actions/checkout@v4
Expand All @@ -29,7 +30,7 @@ jobs:
with:
ocaml-compiler: "4.14"
dune-cache: true
cache-prefix: "v2-bullseye"
cache-prefix: "v3-bookworm"
- name: Install dependencies
working-directory: packages/rescript-relay/rescript-relay-ppx
run: opam install . --deps-only
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ _opam
*.bs.js
rescript-relay-compiler
.now
.DS_Store
.DS_Store
# Local conversion coverage and profiling output
coverage/
*.cpuprofile
conversion-benchmark.json
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# master

- Speed up response and variable conversion with prepared conversion plans and smaller generated instructions. Fix custom-scalar, nested-list, union, and plural `@catch` conversion edge cases; expand regression coverage and add performance/bundle-size benchmarks.

- fix remove-unused-fields command. https://github.com/zth/rescript-relay/pull/636

# 4.5.1
Expand Down
5 changes: 2 additions & 3 deletions build-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ cp .npmignore ../../_release/;
cp ../../README.md ../../_release/;
cp ../../CHANGELOG.md ../../_release/;

# copy and create mjs version of utils.js
cp src/utils.js ../../_release/src/utils.mjs;
sed -i 's/module\.exports \= /export /g' ../../_release/src/utils.mjs;
# Build matching ESM conversion modules from the CommonJS source.
node scripts/build-conversion-modules.js ../../_release/src;

# copy config files
echo "Copying config files..."
Expand Down
2 changes: 1 addition & 1 deletion packages/relay
Submodule relay updated 120 files
131 changes: 131 additions & 0 deletions packages/rescript-relay/__tests__/Test_conversionContract-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
require("@testing-library/jest-dom/extend-expect");
const React = require("react");
const { render, screen, act } = require("@testing-library/react");
const { RelayEnvironmentProvider } = require("react-relay");
const { createMockEnvironment } = require("relay-test-utils");
const { createOperationDescriptor } = require("relay-runtime");
const Scalar = require("./Test_customScalars.bs");
const Fragment = require("./Test_fragment.bs");
const scalarNode =
require("./__generated__/TestCustomScalarsQuery_graphql.bs").node;
const fragmentNode =
require("./__generated__/TestFragmentQuery_graphql.bs").node;

function mount(environment, Component) {
const tree = () =>
React.createElement(
RelayEnvironmentProvider,
{ environment },
React.createElement(
React.Suspense,
{ fallback: "Loading" },
React.createElement(Component),
),
);
const view = render(tree());
return () => view.rerender(tree());
}

test("compiled query converts nested scalar lists and unions after store updates without changing the store", () => {
const environment = createMockEnvironment();
const variables = {
number: [2],
beforeDate: new Date("2018-01-01T00:00:00Z"),
};
const wireVariables = Scalar.Query.convertVariables(variables);
expect(wireVariables).toEqual({
number: 2,
beforeDate: "2018-01-01T00:00:00.000Z",
});
const operation = createOperationDescriptor(scalarNode, wireVariables);
const date1 = "2020-01-01T00:00:00.000Z";
const date2 = "2021-01-01T00:00:00.000Z";
const payload = (date) => ({
loggedInUser: {
id: "viewer",
createdAt: date,
friends: [{ id: "friend", createdAt: date }],
},
member: { __typename: "User", id: "member", createdAt: date },
});
environment.commitPayload(operation, payload(date1));
let converted;
function Probe() {
converted = Scalar.Query.use(variables, "store-only");
return React.createElement(
"div",
null,
converted.loggedInUser.friends[0].createdAt.toISOString(),
);
}
const rerender = mount(environment, Probe);
expect(screen.getByText(date1)).toBeInTheDocument();
expect(converted.member.createdAt).toEqual(new Date(date1));
expect(
environment.lookup(operation.fragment).data.loggedInUser.friends[0]
.createdAt,
).toBe(date1);
const first = converted;
rerender();
expect(converted).toBe(first); // The real binding's useMemo still applies.
act(() => environment.commitPayload(operation, payload(date2)));
expect(screen.getByText(date2)).toBeInTheDocument();
expect(converted).not.toBe(first);
expect(converted.member.createdAt).toEqual(new Date(date2));
expect(first.loggedInUser.friends[0].createdAt).toEqual(new Date(date1));
expect(environment.lookup(operation.fragment).data.member.createdAt).toBe(
date2,
);
});

test("compiled fragment refs can be consumed by nested and plural hooks across nullable store updates", () => {
const environment = createMockEnvironment();
const operation = createOperationDescriptor(fragmentNode, {});
const payload = (status) => ({
loggedInUser: {
id: "viewer",
firstName: "Viewer",
lastName: "Last",
onlineStatus: status,
},
users: {
edges: [
{ node: { id: "friend", firstName: "Friend", onlineStatus: status } },
],
},
});
environment.commitPayload(operation, payload("Online"));
let result;
function Probe() {
const query = Fragment.Query.use(undefined, "store-only");
const fragment = Fragment.Fragment.use(query.loggedInUser.fragmentRefs);
const nested = Fragment.SubFragment.use(fragment.fragmentRefs);
const plural = Fragment.PluralFragment.use(
query.users.edges.map((edge) => edge.node.fragmentRefs),
);
result = { query, fragment, nested, plural };
return React.createElement(
"div",
null,
`${nested.lastName}:${plural[0].onlineStatus ?? "none"}`,
);
}
mount(environment, Probe);
expect(screen.getByText("Last:Online")).toBeInTheDocument();
expect(result.fragment.updatableFragmentRefs).toBe(
result.fragment.fragmentRefs,
);
expect(result.query.users.edges[0].node.updatableFragmentRefs).toBe(
result.query.users.edges[0].node.fragmentRefs,
);
act(() => environment.commitPayload(operation, payload(null)));
expect(screen.getByText("Last:none")).toBeInTheDocument();
expect(result.fragment.onlineStatus).toBeUndefined();
expect(result.plural[0].onlineStatus).toBeUndefined();
// The refs still contain the wire value; only the returned data is converted.
expect(result.query.users.edges[0].node.fragmentRefs.onlineStatus).toBeNull();
expect(
environment.lookup(operation.fragment).data.users.edges[0].node
.onlineStatus,
).toBeNull();
});
90 changes: 90 additions & 0 deletions packages/rescript-relay/__tests__/Test_conversionPlan-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require("@testing-library/jest-dom/extend-expect");
const React = require("react");
const { render, screen, act } = require("@testing-library/react");
const { RelayEnvironmentProvider } = require("react-relay");
const { createMockEnvironment } = require("relay-test-utils");
const { createOperationDescriptor } = require("relay-runtime");
const { Query, ReverseQuery } = require("./Test_conversionPlan.bs");
const generated = require("./__generated__/TestConversionPlanQuery_graphql.bs");

const reverseGenerated = require("./__generated__/TestConversionPlanReverseQuery_graphql.bs");

test.each([
["original", Query, generated],
["reversed", ReverseQuery, reverseGenerated],
])("%s selection order preserves scalars, nulls and nested lists through mounted hooks and writes", (_, Query, generated) => {
const environment = createMockEnvironment();
const variables = {
input: {
grid: [[[1], undefined, [2]], undefined],
children: [{ grid: [[[3]]] }, undefined],
},
};
const convertedVariables = Query.convertVariables(variables);
expect(JSON.parse(JSON.stringify(convertedVariables))).toStrictEqual({
input: { grid: [[1, null, 2], null], children: [{ grid: [[3]] }, null] },
});
const operation = createOperationDescriptor(
generated.node,
convertedVariables,
);
const date = "2020-01-01T00:00:00.000Z";
const raw = {
conversionContract: {
grid: [[1, null, 2], null],
dates: [date, null],
raw: [[[null, { x: null }], null], null],
a_b: 7,
a: { b: date },
},
};
environment.commitPayload(operation, raw);
let data;
function Probe() {
data = Query.use(variables, "store-only");
return React.createElement(
"div",
null,
data.conversionContract.a?.b?.toISOString() ?? "empty",
);
}
render(
React.createElement(
RelayEnvironmentProvider,
{ environment },
React.createElement(
React.Suspense,
{ fallback: "loading" },
React.createElement(Probe),
),
),
);
expect(screen.getByText(date)).toBeInTheDocument();
expect(data.conversionContract).toStrictEqual({
grid: [[[1], undefined, [2]], undefined],
dates: [new Date(date), undefined],
raw: [[[null, { x: null }], undefined], undefined],
a_b: [7],
a: { b: new Date(date) },
});
expect(environment.lookup(operation.fragment).data).toStrictEqual(raw);
expect(generated.Internal.convertWrapResponse(data)).toStrictEqual(raw);
act(() =>
environment.commitPayload(operation, {
conversionContract: {
grid: null,
dates: null,
raw: null,
a_b: null,
a: null,
},
}),
);
expect(data.conversionContract).toStrictEqual({
grid: undefined,
dates: undefined,
raw: undefined,
a_b: undefined,
a: undefined,
});
});
23 changes: 23 additions & 0 deletions packages/rescript-relay/__tests__/Test_conversionPlan.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Query = %relay(`
query TestConversionPlanQuery($input: ConversionContractInput) {
conversionContract(input: $input) {
grid
dates
raw
a_b
a { b }
}
}
`)

module ReverseQuery = %relay(`
query TestConversionPlanReverseQuery($input: ConversionContractInput) {
conversionContract(input: $input) {
a { b }
a_b
raw
dates
grid
}
}
`)
Loading
Loading