New
- Support
@fieldPolicykey paths through a list of input objects (#1097): AkeyArgspath can now pass through one list level of input objects, such askeyArgs: "inputs.id"onheroes(inputs: [HeroInput!]!): [Hero!]!, and produces one cache key per input object (Hero:1,Hero:2, …). Previously, codegen rejected these paths and the runtime resolved them to no keys. Paths with a second list, or with more than one list-valued key argument, are still rejected with a descriptive error.
Improvement
- Fail the request when a
GraphQLInterceptordoes not callnext(#1074): An interceptor that returned its own results without callingnextleft the request chain without a final request, which crashed on a force unwrap — even for mutations, subscriptions, and requests that never write to the cache. The chain now fails the request withRequestChain.Error.interceptorDidNotCallNext, naming the offending interceptor and operation. Callingnextremains required; skipping it would silently skipfetchBehaviorand every later interceptor. Fixes #3654.
Fixed
- Resolve field policy cache keys to the records they name (#1091): A key produced by a field policy (
@fieldPolicyor aFieldPolicy.Provider) was looked up as a field on the parent record, which the normalizer never writes. A policy read of a single object always missed, and a policy read of a list dropped every key and succeeded with[], so.cacheFirstreturned an empty list without making a network request. The key now resolves to the keyed record like any other cache reference, and a key whose record isn't cached is a cache miss. Fixes #3659. Thank you to @drewmclean for the contribution. - Read stored field values before applying field policies (#1098): A field with a field policy was always read from the record its policy key named, never from the value a network fetch stored for the field's arguments. When the policy key and the stored record's key differed, such as a type policy keyed by
idalongside a field policy keyed by name, every read missed and went to the network, even right after fetching the same arguments. The stored value is now read first, and the field policy applies when none is stored. - Fix
AsyncReadWriteLockallowing two writers to run at once (#1093): When a writer finished, the readers woken from the queue counted themselves only after re-entering the actor. A higher-priority reader could finish before a lower-priority one resumed, dropping the running count to zero and waking the next writer while a reader was still active. Reads and writes could then overlap, which crashed inApolloStore.unsubscribethrough concurrent mutation of its subscribers dictionary. Woken readers are now counted before they are resumed. Thank you to @pixelmatrix for the contribution. - Preserve arrival order in
AsyncReadWriteLock(#1096): A reader arriving while a read batch was queued ahead of a writer joined that batch and ran before the waiting writer. New readers now join the last queued item, or start a new batch behind a writer, so a stream of readers can no longer pass queued writers. Reads and writes never overlapped in this case. Thank you to @carllindberg for the review note. - Ignore stale
WebSocketTransportreceive loop events afterpause()/resume()(#1089): Aresume()landing between a receive loop's "is this connection current" check and its disconnect handling let the old loop handle its disconnection against the new connection. A subscription could then be finished as.completedinstead of being re-subscribed on the new connection. Thank you to @3redrubies for the contribution. - Create
WebSocketTransporttasks only when connecting (#1102): The transport created itsURLSessionWebSocketTaskininitbut did not resume it until the first subscription orresume().URLSessionmeasurestimeoutIntervalForResource(7 days by default) from task creation, so a process that stayed alive for a week before its first subscription crashed inside CFNetwork on the first send or receive. Each task is now created immediately before it is resumed. As a result,WebSocketURLSession.webSocketTask(with:)is called at connect time rather than ininit, and header and connecting-payload updates made before the first connection now apply to it. Thank you to @3redrubies for the contribution. - Fix codegen pruning of files written through symlinked paths (#1100): When the output directory was reached through a symlink (for example
/tmp→/private/tmp), the schema module file was written with one path spelling and found by pruning with another, so it was pruned right after being generated on the second run. Paths are now normalized in one place, resolving symlinks in the containing directory. Builds on #1090. Thank you to @carllindberg for the contribution. - Honor
Globexclude patterns that don't contain a globstar (#1101): An exclude such as!./Legacy/Old.graphqlor!/abs/path/file.graphqllost its leading!and became an include path that matched nothing, so the exclusion was silently ignored. Only the**form worked. The!is now kept for these patterns. - Remove a stray parenthesis from
GraphQLExecutionError.errorDescription(#1103): The description rendered asError at path "a.b)": …because of an extra)after the path.