Skip to content

Commit de97938

Browse files
jwaldripclaude
andcommitted
feat: make @defer/@stream directives opt-in
Move @defer and @stream directives from core built-ins to a new opt-in module Absinthe.Type.BuiltIns.IncrementalDirectives. Since @defer/@stream are draft-spec features (not yet finalized), users must now explicitly opt-in by adding: import_types Absinthe.Type.BuiltIns.IncrementalDirectives to their schema definition. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 204257f commit de97938

File tree

7 files changed

+142
-70
lines changed

7 files changed

+142
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
* **draft-spec:** Add `@defer` and `@stream` directives for incremental delivery ([#1377](https://github.com/absinthe-graphql/absinthe/pull/1377))
88
- **Note:** These directives are still in draft/RFC stage and not yet part of the finalized GraphQL specification
9+
- **Opt-in required:** `import_types Absinthe.Type.BuiltIns.IncrementalDirectives` in your schema
910
- Split GraphQL responses into initial + incremental payloads
1011
- Configure via `Absinthe.Pipeline.Incremental.enable/2`
1112
- Resource limits (max concurrent streams, memory, duration)

guides/incremental-delivery.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@ def deps do
2929
end
3030
```
3131

32+
## Schema Setup
33+
34+
Since `@defer` and `@stream` are draft-spec features, you must explicitly opt-in by importing the directives in your schema:
35+
36+
```elixir
37+
defmodule MyApp.Schema do
38+
use Absinthe.Schema
39+
40+
# Import the draft-spec @defer and @stream directives
41+
import_types Absinthe.Type.BuiltIns.IncrementalDirectives
42+
43+
query do
44+
# ...
45+
end
46+
end
47+
```
48+
49+
Without this import, the `@defer` and `@stream` directives will not be available in your schema.
50+
3251
## Basic Usage
3352

3453
### The @defer Directive

lib/absinthe/type/built_ins/directives.ex

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -43,74 +43,4 @@ defmodule Absinthe.Type.BuiltIns.Directives do
4343
Blueprint.put_flag(node, :include, __MODULE__)
4444
end
4545
end
46-
47-
directive :defer do
48-
description """
49-
Directs the executor to defer this fragment spread or inline fragment,
50-
delivering it as part of a subsequent response. Used to improve latency
51-
for data that is not immediately required.
52-
"""
53-
54-
repeatable false
55-
56-
arg :if, :boolean,
57-
default_value: true,
58-
description: "When true, fragment may be deferred. When false, fragment will not be deferred and data will be included in the initial response. Defaults to true."
59-
60-
arg :label, :string,
61-
description: "A unique label for this deferred fragment, used to identify it in the incremental response."
62-
63-
on [:fragment_spread, :inline_fragment]
64-
65-
expand fn
66-
%{if: false}, node ->
67-
# Don't defer when if: false
68-
node
69-
70-
args, node ->
71-
# Mark node for deferred execution
72-
defer_config = %{
73-
label: Map.get(args, :label),
74-
enabled: true
75-
}
76-
Blueprint.put_flag(node, :defer, defer_config)
77-
end
78-
end
79-
80-
directive :stream do
81-
description """
82-
Directs the executor to stream list fields, delivering list items incrementally
83-
in multiple responses. Used to improve latency for large lists.
84-
"""
85-
86-
repeatable false
87-
88-
arg :if, :boolean,
89-
default_value: true,
90-
description: "When true, list field may be streamed. When false, list will not be streamed and all data will be included in the initial response. Defaults to true."
91-
92-
arg :label, :string,
93-
description: "A unique label for this streamed field, used to identify it in the incremental response."
94-
95-
arg :initial_count, :integer,
96-
default_value: 0,
97-
description: "The number of list items to return in the initial response. Defaults to 0."
98-
99-
on [:field]
100-
101-
expand fn
102-
%{if: false}, node ->
103-
# Don't stream when if: false
104-
node
105-
106-
args, node ->
107-
# Mark node for streaming execution
108-
stream_config = %{
109-
label: Map.get(args, :label),
110-
initial_count: Map.get(args, :initial_count, 0),
111-
enabled: true
112-
}
113-
Blueprint.put_flag(node, :stream, stream_config)
114-
end
115-
end
11646
end
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
defmodule Absinthe.Type.BuiltIns.IncrementalDirectives do
2+
@moduledoc """
3+
Draft-spec incremental delivery directives: @defer and @stream.
4+
5+
These directives are part of the [Incremental Delivery RFC](https://github.com/graphql/graphql-spec/blob/main/rfcs/DeferStream.md)
6+
and are not yet part of the finalized GraphQL specification.
7+
8+
## Usage
9+
10+
To enable @defer and @stream in your schema, import this module:
11+
12+
defmodule MyApp.Schema do
13+
use Absinthe.Schema
14+
15+
import_types Absinthe.Type.BuiltIns.IncrementalDirectives
16+
17+
query do
18+
# ...
19+
end
20+
end
21+
22+
You will also need to enable incremental delivery in your pipeline:
23+
24+
pipeline_modifier = fn pipeline, _options ->
25+
Absinthe.Pipeline.Incremental.enable(pipeline,
26+
enabled: true,
27+
enable_defer: true,
28+
enable_stream: true
29+
)
30+
end
31+
32+
Absinthe.run(query, MyApp.Schema,
33+
variables: variables,
34+
pipeline_modifier: pipeline_modifier
35+
)
36+
37+
## Directives
38+
39+
- `@defer` - Defers execution of a fragment spread or inline fragment
40+
- `@stream` - Streams list field items incrementally
41+
"""
42+
43+
use Absinthe.Schema.Notation
44+
45+
alias Absinthe.Blueprint
46+
47+
directive :defer do
48+
description """
49+
Directs the executor to defer this fragment spread or inline fragment,
50+
delivering it as part of a subsequent response. Used to improve latency
51+
for data that is not immediately required.
52+
"""
53+
54+
repeatable false
55+
56+
arg :if, :boolean,
57+
default_value: true,
58+
description: "When true, fragment may be deferred. When false, fragment will not be deferred and data will be included in the initial response. Defaults to true."
59+
60+
arg :label, :string,
61+
description: "A unique label for this deferred fragment, used to identify it in the incremental response."
62+
63+
on [:fragment_spread, :inline_fragment]
64+
65+
expand fn
66+
%{if: false}, node ->
67+
# Don't defer when if: false
68+
node
69+
70+
args, node ->
71+
# Mark node for deferred execution
72+
defer_config = %{
73+
label: Map.get(args, :label),
74+
enabled: true
75+
}
76+
Blueprint.put_flag(node, :defer, defer_config)
77+
end
78+
end
79+
80+
directive :stream do
81+
description """
82+
Directs the executor to stream list fields, delivering list items incrementally
83+
in multiple responses. Used to improve latency for large lists.
84+
"""
85+
86+
repeatable false
87+
88+
arg :if, :boolean,
89+
default_value: true,
90+
description: "When true, list field may be streamed. When false, list will not be streamed and all data will be included in the initial response. Defaults to true."
91+
92+
arg :label, :string,
93+
description: "A unique label for this streamed field, used to identify it in the incremental response."
94+
95+
arg :initial_count, :integer,
96+
default_value: 0,
97+
description: "The number of list items to return in the initial response. Defaults to 0."
98+
99+
on [:field]
100+
101+
expand fn
102+
%{if: false}, node ->
103+
# Don't stream when if: false
104+
node
105+
106+
args, node ->
107+
# Mark node for streaming execution
108+
stream_config = %{
109+
label: Map.get(args, :label),
110+
initial_count: Map.get(args, :initial_count, 0),
111+
enabled: true
112+
}
113+
Blueprint.put_flag(node, :stream, stream_config)
114+
end
115+
end
116+
end

test/absinthe/incremental/complexity_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ defmodule Absinthe.Incremental.ComplexityTest do
1616
defmodule TestSchema do
1717
use Absinthe.Schema
1818

19+
import_types Absinthe.Type.BuiltIns.IncrementalDirectives
20+
1921
query do
2022
field :user, :user do
2123
resolve fn _, _ -> {:ok, %{id: "1", name: "Test User"}} end

test/absinthe/incremental/defer_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ defmodule Absinthe.Incremental.DeferTest do
1414
defmodule TestSchema do
1515
use Absinthe.Schema
1616

17+
import_types Absinthe.Type.BuiltIns.IncrementalDirectives
18+
1719
query do
1820
field :user, :user do
1921
arg :id, non_null(:id)

test/absinthe/incremental/stream_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ defmodule Absinthe.Incremental.StreamTest do
1414
defmodule TestSchema do
1515
use Absinthe.Schema
1616

17+
import_types Absinthe.Type.BuiltIns.IncrementalDirectives
18+
1719
query do
1820
field :users, list_of(:user) do
1921
resolve fn _, _ ->

0 commit comments

Comments
 (0)