ruact 0.0.4 → 0.0.6

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +54 -2
  3. data/.rubocop_todo.yml +3 -115
  4. data/CHANGELOG.md +68 -17
  5. data/bench/server_functions_dispatch_bench.rb +109 -142
  6. data/bench/server_functions_dispatch_bench.results.md +29 -0
  7. data/docs/internal/decisions/server-functions-api.md +402 -0
  8. data/lib/generators/ruact/install/install_generator.rb +310 -25
  9. data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
  10. data/lib/generators/ruact/install/templates/dev.tt +16 -0
  11. data/lib/generators/ruact/install/templates/package.json.tt +17 -0
  12. data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
  13. data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
  14. data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
  15. data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
  16. data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
  17. data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
  18. data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
  19. data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
  20. data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
  21. data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
  22. data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
  23. data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
  24. data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
  25. data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
  26. data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
  27. data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
  28. data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
  29. data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
  30. data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
  31. data/lib/ruact/client_manifest.rb +37 -36
  32. data/lib/ruact/component_contract.rb +115 -0
  33. data/lib/ruact/configuration.rb +80 -28
  34. data/lib/ruact/controller.rb +69 -421
  35. data/lib/ruact/doctor.rb +133 -4
  36. data/lib/ruact/erb_preprocessor.rb +65 -12
  37. data/lib/ruact/erb_preprocessor_hook.rb +4 -1
  38. data/lib/ruact/errors.rb +30 -44
  39. data/lib/ruact/html_converter.rb +22 -1
  40. data/lib/ruact/railtie.rb +56 -200
  41. data/lib/ruact/server.rb +28 -6
  42. data/lib/ruact/server_functions/codegen.rb +49 -188
  43. data/lib/ruact/server_functions/codegen_v2.rb +23 -6
  44. data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
  45. data/lib/ruact/server_functions/error_payload.rb +1 -1
  46. data/lib/ruact/server_functions/error_rendering.rb +22 -29
  47. data/lib/ruact/server_functions/name_bridge.rb +14 -16
  48. data/lib/ruact/server_functions/query_source.rb +35 -8
  49. data/lib/ruact/server_functions/route_source.rb +3 -4
  50. data/lib/ruact/server_functions/snapshot.rb +12 -139
  51. data/lib/ruact/server_functions/validation_errors.rb +70 -0
  52. data/lib/ruact/server_functions.rb +21 -25
  53. data/lib/ruact/signed_references.rb +162 -0
  54. data/lib/ruact/string_distance.rb +72 -0
  55. data/lib/ruact/validation_errors_collector.rb +139 -0
  56. data/lib/ruact/version.rb +1 -1
  57. data/lib/ruact/view_helper.rb +102 -0
  58. data/lib/ruact.rb +19 -19
  59. data/lib/tasks/ruact.rake +10 -53
  60. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
  61. data/spec/ruact/client_manifest_spec.rb +36 -0
  62. data/spec/ruact/component_contract_spec.rb +119 -0
  63. data/spec/ruact/configuration_spec.rb +51 -34
  64. data/spec/ruact/controller_request_spec.rb +264 -0
  65. data/spec/ruact/controller_spec.rb +63 -326
  66. data/spec/ruact/doctor_spec.rb +201 -0
  67. data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
  68. data/spec/ruact/erb_preprocessor_spec.rb +127 -0
  69. data/spec/ruact/errors_spec.rb +0 -45
  70. data/spec/ruact/html_converter_spec.rb +50 -0
  71. data/spec/ruact/install_generator_spec.rb +591 -4
  72. data/spec/ruact/query_request_spec.rb +109 -1
  73. data/spec/ruact/scaffold_generator_spec.rb +1835 -0
  74. data/spec/ruact/server_bucket_request_spec.rb +142 -0
  75. data/spec/ruact/server_function_name_spec.rb +1 -1
  76. data/spec/ruact/server_functions/codegen_spec.rb +158 -269
  77. data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
  78. data/spec/ruact/server_functions/query_source_spec.rb +51 -0
  79. data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
  80. data/spec/ruact/server_functions/rake_spec.rb +29 -29
  81. data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
  82. data/spec/ruact/server_rescue_request_spec.rb +6 -6
  83. data/spec/ruact/server_spec.rb +8 -9
  84. data/spec/ruact/signed_references_spec.rb +164 -0
  85. data/spec/ruact/string_distance_spec.rb +38 -0
  86. data/spec/ruact/validation_errors_spec.rb +116 -0
  87. data/spec/ruact/view_helper_spec.rb +79 -0
  88. data/spec/spec_helper.rb +0 -5
  89. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +44 -47
  90. data/vendor/javascript/ruact-server-functions-runtime/index.js +151 -107
  91. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
  92. data/vendor/javascript/ruact-server-functions-runtime/package.json +2 -2
  93. data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +187 -0
  94. data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
  95. data/vendor/javascript/vite-plugin-ruact/index.js +353 -7
  96. data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
  97. data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
  98. data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
  99. data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
  100. data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
  101. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
  102. data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
  103. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
  104. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
  105. data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
  106. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
  107. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
  108. data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
  109. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
  110. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
  111. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
  112. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
  113. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
  114. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
  115. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
  116. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
  117. data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
  118. data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
  119. metadata +55 -15
  120. data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
  121. data/lib/ruact/server_action.rb +0 -131
  122. data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
  123. data/lib/ruact/server_functions/registry.rb +0 -148
  124. data/lib/ruact/server_functions/registry_entry.rb +0 -26
  125. data/lib/ruact/server_functions/standalone_context.rb +0 -103
  126. data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
  127. data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
  128. data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
  129. data/spec/ruact/server_functions/registry_spec.rb +0 -199
  130. data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
  131. data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
  132. data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Story 13.3 (FR98) — unit specs for the pure validation-error normalizer
4
+ # Ruact::ServerFunctions::ValidationErrors.normalize. The normalizer turns an
5
+ # ActiveModel-ish record, a raw ActiveModel::Errors, or a pre-shaped Hash into
6
+ # the canonical wire shape `{ [attribute: String] => Array<String> }` (full
7
+ # messages, `base` key, `{}` on a valid/empty/nil source, idempotent on
8
+ # already-canonical input). Pure — no Rails/request/Ruact.config reads.
9
+
10
+ require "spec_helper"
11
+ require "active_model"
12
+
13
+ require "ruact/server_functions/validation_errors"
14
+
15
+ module Ruact
16
+ module ServerFunctions
17
+ RSpec.describe ValidationErrors, :story_13_3 do
18
+ # A minimal ActiveModel record so the normalizer can exercise the real
19
+ # `errors.group_by_attribute` / `ActiveModel::Error#full_message` API
20
+ # (present since Rails 6.1; gem pins `rails ~> 8.0`).
21
+ let(:record_class) do
22
+ Class.new do
23
+ include ActiveModel::Model
24
+ include ActiveModel::Attributes
25
+
26
+ attribute :title, :string
27
+ attribute :body, :string
28
+
29
+ def self.name = "ValidationErrorsSpecPost"
30
+ end
31
+ end
32
+
33
+ describe ".normalize" do
34
+ context "with a record responding to #errors" do
35
+ it "returns the canonical { attr => [full_message] } shape" do
36
+ record = record_class.new
37
+ record.errors.add(:title, :blank) # "Title can't be blank"
38
+
39
+ expect(described_class.normalize(record)).to eq("title" => ["Title can't be blank"])
40
+ end
41
+
42
+ it "keys base-level errors under the string 'base' with the bare message" do
43
+ record = record_class.new
44
+ record.errors.add(:base, "is invalid overall")
45
+
46
+ expect(described_class.normalize(record)).to eq("base" => ["is invalid overall"])
47
+ end
48
+
49
+ it "groups multiple attributes and multiple messages per attribute" do
50
+ record = record_class.new
51
+ record.errors.add(:title, :blank) # "Title can't be blank"
52
+ record.errors.add(:title, "is too short") # "Title is too short"
53
+ record.errors.add(:body, "is invalid") # "Body is invalid"
54
+
55
+ expect(described_class.normalize(record)).to eq(
56
+ "title" => ["Title can't be blank", "Title is too short"],
57
+ "body" => ["Body is invalid"]
58
+ )
59
+ end
60
+
61
+ it "returns {} for a record with no errors (success symmetry)" do
62
+ expect(described_class.normalize(record_class.new)).to eq({})
63
+ end
64
+ end
65
+
66
+ context "with a raw ActiveModel::Errors" do
67
+ it "normalizes the Errors object directly" do
68
+ record = record_class.new
69
+ record.errors.add(:title, :blank)
70
+
71
+ expect(described_class.normalize(record.errors)).to eq("title" => ["Title can't be blank"])
72
+ end
73
+ end
74
+
75
+ context "with a pre-shaped Hash" do
76
+ it "stringifies keys and leaves canonical array values untouched (idempotent)" do
77
+ canonical = { "title" => ["Title can't be blank"] }
78
+
79
+ expect(described_class.normalize(canonical)).to eq(canonical)
80
+ end
81
+
82
+ it "stringifies symbol keys" do
83
+ expect(described_class.normalize(title: ["x"])).to eq("title" => ["x"])
84
+ end
85
+
86
+ it "coerces a scalar value into a one-element array" do
87
+ expect(described_class.normalize("title" => "can't be blank")).to eq("title" => ["can't be blank"])
88
+ end
89
+
90
+ it "coerces non-string message values to strings" do
91
+ expect(described_class.normalize("count" => [1, 2])).to eq("count" => %w[1 2])
92
+ end
93
+
94
+ it "returns {} for an empty Hash" do
95
+ expect(described_class.normalize({})).to eq({})
96
+ end
97
+ end
98
+
99
+ context "with nil / empty sources" do
100
+ it "returns {} for nil" do
101
+ expect(described_class.normalize(nil)).to eq({})
102
+ end
103
+ end
104
+
105
+ it "is idempotent — normalizing its own output yields the same Hash" do
106
+ record = record_class.new
107
+ record.errors.add(:title, :blank)
108
+ record.errors.add(:base, "is invalid overall")
109
+
110
+ once = described_class.normalize(record)
111
+ expect(described_class.normalize(once)).to eq(once)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
  require "active_support/core_ext/string/output_safety"
5
+ require "active_support/string_inquirer"
5
6
 
6
7
  module Ruact
7
8
  RSpec.describe ViewHelper do
@@ -48,5 +49,83 @@ module Ruact
48
49
  .to raise_error(Ruact::Error, /__ruact_component__ called outside a ruact_render flow/)
49
50
  end
50
51
  end
52
+
53
+ # Story 14.2 (FR104) — the public JS-asset helper. Emits the dev/prod
54
+ # bootstrap entry `<script>` tags (re-targeting `virtual:ruact/bootstrap`)
55
+ # plus the `__FLIGHT_DATA` inline script. The controller delegates to this
56
+ # one implementation (parity asserted in controller_spec).
57
+ describe "#ruact_js_assets", :story_14_2 do
58
+ let(:asset_helper) do
59
+ obj = Object.new
60
+ obj.extend(described_class)
61
+ obj
62
+ end
63
+ let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
64
+
65
+ context "when in dev with the Vite dev server running" do
66
+ before do
67
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
68
+ allow(asset_helper).to receive(:vite_dev_running?).and_return(true)
69
+ end
70
+
71
+ it "emits the react-refresh preamble, @vite/client, and the bootstrap module", :aggregate_failures do
72
+ html = asset_helper.ruact_js_assets(payload)
73
+ expect(html).to include("__vite_plugin_react_preamble_installed__")
74
+ expect(html).to include("http://localhost:5173/@vite/client")
75
+ # Targets the virtual entry at the dev server's /@id/__x00__ URL — NOT
76
+ # a stale application.jsx path.
77
+ expect(html).to include("/@id/__x00__#{Ruact.bootstrap_virtual_id}")
78
+ expect(html).not_to include("application.jsx")
79
+ end
80
+
81
+ it "includes the __FLIGHT_DATA inline bootstrap script", :aggregate_failures do
82
+ html = asset_helper.ruact_js_assets(payload)
83
+ expect(html).to include("__FLIGHT_DATA")
84
+ expect(html).to include("d.push(")
85
+ end
86
+
87
+ it "returns an html_safe buffer" do
88
+ expect(asset_helper.ruact_js_assets(payload)).to be_html_safe
89
+ end
90
+ end
91
+
92
+ context "when in prod (Vite manifest lookup)" do
93
+ before do
94
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
95
+ end
96
+
97
+ it "looks up the manifest with the SAME id the generated vite.config input uses (AC3 — no drift)" do
98
+ allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
99
+ asset_helper.ruact_js_assets(payload)
100
+ expect(asset_helper).to have_received(:vite_manifest_entry).with(Ruact.bootstrap_virtual_id)
101
+ end
102
+
103
+ it "emits the hashed bootstrap URL from the manifest entry", :aggregate_failures do
104
+ allow(asset_helper).to receive(:vite_manifest_entry)
105
+ .with(Ruact.bootstrap_virtual_id).and_return({ "file" => "bootstrap-abc123.js" })
106
+ html = asset_helper.ruact_js_assets(payload)
107
+ expect(html).to include(%(src="/assets/bootstrap-abc123.js"))
108
+ expect(html).to include("__FLIGHT_DATA")
109
+ end
110
+
111
+ it "falls back to /assets/application.js when the manifest entry is missing" do
112
+ allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
113
+ expect(asset_helper.ruact_js_assets(payload)).to include("/assets/application.js")
114
+ end
115
+ end
116
+
117
+ context "without a Flight payload (entry tags only)" do
118
+ before do
119
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
120
+ allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
121
+ end
122
+
123
+ it "emits only the entry tag (no __FLIGHT_DATA)", :aggregate_failures do
124
+ html = asset_helper.ruact_js_assets
125
+ expect(html).not_to include("__FLIGHT_DATA")
126
+ expect(html).to include("<script type=\"module\"")
127
+ end
128
+ end
129
+ end
51
130
  end
52
131
  end
data/spec/spec_helper.rb CHANGED
@@ -50,11 +50,6 @@ RSpec.configure do |config|
50
50
  config.before do
51
51
  Ruact.instance_variable_set(:@config, nil)
52
52
  Ruact.instance_variable_set(:@configured_at_least_once, false)
53
- # Story 8.0a: both server-function registries are lazy-initialized module
54
- # singletons. Wipe them between examples so register/clear specs in one file
55
- # cannot bleed entries into another under random order.
56
- Ruact.instance_variable_set(:@action_registry, nil)
57
- Ruact.instance_variable_set(:@query_registry, nil)
58
53
  # Story 9.3: specs that set `Rails.logger = instance_double(Logger)` (e.g.
59
54
  # railtie_spec, configuration_spec) leave a per-example double on the global
60
55
  # after they finish; rspec then refuses to call it from the NEXT example.
@@ -1,15 +1,11 @@
1
- // Story 8.1 — TypeScript declarations for the real server-functions runtime.
1
+ // TypeScript declarations for the route-driven server-functions runtime.
2
2
  // Mirrors the JS exports in `index.js` so the generated module's
3
- // `import { _makeRef } from "ruact/server-functions-runtime"` resolves under
4
- // `tsc --noEmit` (AC10's import guarantee).
3
+ // `import { _makeServerFunction } from "ruact/server-functions-runtime"`
4
+ // resolves under `tsc --noEmit`. Story 9.9 demolished the v1 `_makeRef` accessor.
5
5
  //
6
- // The generated module's per-export signature is
7
- // `(args?: Record<string, unknown>) => Promise<unknown>` per the 8.0a
8
- // codegen contract; the runtime accepts a wider `FormData` argument too
9
- // (Story 8.2 owns the codegen signature widening if it picks the FormData
10
- // path). Devs writing call sites against the 8.0a-emitted module continue
11
- // to see the conservative Record<string, unknown> signature; the FormData
12
- // branch only fires through Story 8.2's `<form action={fn}>` wiring.
6
+ // The runtime accepts a wider `FormData` argument as well as the conservative
7
+ // `Record<string, unknown>` shape; the FormData branch fires through the
8
+ // `<form action={fn}>` wiring.
13
9
 
14
10
  /**
15
11
  * Re-run-3 (2026-05-15), refined Re-run-4 (2026-05-15) — local alias
@@ -36,46 +32,25 @@ type RuactResponse = typeof globalThis extends { Response: new (...args: never[]
36
32
  : unknown;
37
33
 
38
34
  /**
39
- * Returns a callable accessor for a server function registered with the
40
- * given Ruby symbol name. The accessor, when invoked, POSTs the args to
41
- * `/__ruact/fn/${name}`.
42
- *
43
- * Story 8.2 (refined 2026-05-17 per review patch R1) — the return type
44
- * is an intersection of FOUR call signatures so the same exported
45
- * reference is usable from every call site:
46
- *
47
- * 1. `()` / `(args)` / `(prevState, formData)` — direct callers and
48
- * `useActionState`'s two-arg invocation; returns `Promise<unknown>`.
49
- * 2. `(formData: FormData)` — assignable to React 19's `<form action>`
50
- * prop, which is typed as `(formData: FormData) => void | Promise<void>`.
51
- * Promise generics are invariant in TS, so `Promise<unknown>` is
52
- * NOT assignable to `Promise<void>` even via the void-discard rule;
53
- * the intersection lets `<form action={createPost}>` typecheck
54
- * DIRECTLY against the emitted module without a call-site cast.
55
- *
56
- * Runtime behavior is unchanged — `_makeRef` always resolves with the
57
- * JSON-decoded value. The `Promise<void>` overload is a TYPE-ONLY
58
- * surface: when React invokes the function from a `<form action>` prop,
59
- * the return value is discarded by React anyway.
60
- */
61
- export function _makeRef(
62
- name: string,
63
- ): ((
64
- arg1?: Record<string, unknown> | RuactFormData,
65
- arg2?: RuactFormData | Record<string, unknown>,
66
- ) => Promise<unknown>) &
67
- ((formData: RuactFormData) => Promise<void>);
68
-
69
- /**
70
- * Story 9.3 — the route-driven (v2) accessor. The codegen emits
35
+ * Story 9.3 the route-driven accessor. The codegen emits
71
36
  * `_makeServerFunction({ method, path, segments })` for every non-GET routed
72
37
  * action on a `Ruact::Server` controller. The returned callable targets the
73
38
  * REAL Rails route + verb (e.g. `POST /posts`, `PUT /posts/:id`), interpolating
74
39
  * dynamic path segments by name from the single call argument, and follows a
75
40
  * Bucket-2 `{ "$redirect": "<path>" }` response client-side.
76
41
  *
77
- * Shares the same intersection call-signature contract as {@link _makeRef} so
78
- * `<form action={createPost}>` and `useActionState` keep type-checking.
42
+ * Story 8.2 (refined 2026-05-17 per review patch R1) — the return type is an
43
+ * intersection of call signatures so the same exported reference is usable from
44
+ * every call site:
45
+ *
46
+ * 1. `()` / `(args)` / `(prevState, formData)` — direct callers and
47
+ * `useActionState`'s two-arg invocation; returns `Promise<unknown>`.
48
+ * 2. `(formData: FormData)` — assignable to React 19's `<form action>`
49
+ * prop, which is typed as `(formData: FormData) => void | Promise<void>`.
50
+ * Promise generics are invariant in TS, so `Promise<unknown>` is NOT
51
+ * assignable to `Promise<void>` even via the void-discard rule; the
52
+ * intersection lets `<form action={createPost}>` typecheck DIRECTLY
53
+ * against the emitted module without a call-site cast.
79
54
  */
80
55
  export function _makeServerFunction(descriptor: {
81
56
  method: string;
@@ -113,11 +88,33 @@ export function _makeQuery(descriptor: {
113
88
  * resolution; `error` carries the structured {@link RuactActionError} on
114
89
  * failure. A superseded in-flight response is dropped.
115
90
  *
116
- * Request de-duplication across components is Story 9.6; this hook fetches
117
- * once per mount.
91
+ * Story 9.6 identical concurrent calls (same reference + same params,
92
+ * order-independent) share ONE in-flight request. Dedup is in-flight only:
93
+ * once a request settles the shared entry is dropped, so a fresh mount
94
+ * refetches (no TTL cache, no stale-while-revalidate).
95
+ *
96
+ * Story 13.4 — the `reference` parameter accepts ANY codegen-emitted accessor
97
+ * shape: the pre-13.4 declaration typed it `(params?: Record<string, unknown>)
98
+ * => Promise<unknown>`, which a now-TYPED query accessor (e.g. `(params: { term:
99
+ * string | number | boolean | null }) => Promise<unknown>`) is NOT assignable to
100
+ * under `strict` (contravariant param mismatch). Widening to `(...args: never[])
101
+ * => Promise<unknown>` accepts every typed/untyped/no-kwargs accessor without a
102
+ * cast and without a regression. `T` (the return type) stays the explicitly-
103
+ * specifiable return-typing generic — `useQuery<User[]>(searchUsers, { term })`.
104
+ *
105
+ * SCOPE BOUNDARY (intentional): the second `params` argument stays
106
+ * `Record<string, unknown>` — `useQuery` does NOT re-check the params SHAPE
107
+ * against the accessor. Per-param compile-time checking (missing-required,
108
+ * unknown-key, wrong-typed) is delivered at the ACCESSOR's own call site
109
+ * (`searchUsers({ term })`), which is where FR99 narrows the type. Inferring the
110
+ * params type here would require a second generic `P` that is impossible to make
111
+ * both inferred-from-the-accessor AND compatible with the explicit
112
+ * `useQuery<User[]>(…)` return-typing form (TS short-circuits inference of a
113
+ * defaulted trailing type parameter, and rejects a partial type-argument list
114
+ * for a non-defaulted one). This matches the pre-13.4 `params` contract exactly.
118
115
  */
119
116
  export function useQuery<T = unknown>(
120
- reference: (params?: Record<string, unknown>) => Promise<unknown>,
117
+ reference: (...args: never[]) => Promise<unknown>,
121
118
  params?: Record<string, unknown>,
122
119
  ): { data: T | undefined; loading: boolean; error: unknown };
123
120