@isograph/react 0.3.1 → 0.4.0

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 (119) hide show
  1. package/.turbo/turbo-compile-libs.log +5 -0
  2. package/dist/core/FragmentReference.d.ts +5 -5
  3. package/dist/core/FragmentReference.d.ts.map +1 -1
  4. package/dist/core/IsographEnvironment.d.ts +14 -9
  5. package/dist/core/IsographEnvironment.d.ts.map +1 -1
  6. package/dist/core/PromiseWrapper.d.ts +4 -4
  7. package/dist/core/PromiseWrapper.d.ts.map +1 -1
  8. package/dist/core/PromiseWrapper.js +2 -9
  9. package/dist/core/areEqualWithDeepComparison.d.ts +1 -3
  10. package/dist/core/areEqualWithDeepComparison.d.ts.map +1 -1
  11. package/dist/core/areEqualWithDeepComparison.js +0 -2
  12. package/dist/core/brand.d.ts +2 -0
  13. package/dist/core/brand.d.ts.map +1 -0
  14. package/dist/core/brand.js +2 -0
  15. package/dist/core/cache.d.ts +7 -6
  16. package/dist/core/cache.d.ts.map +1 -1
  17. package/dist/core/cache.js +46 -28
  18. package/dist/core/check.d.ts +3 -3
  19. package/dist/core/check.d.ts.map +1 -1
  20. package/dist/core/componentCache.d.ts.map +1 -1
  21. package/dist/core/componentCache.js +1 -1
  22. package/dist/core/entrypoint.d.ts +20 -2
  23. package/dist/core/entrypoint.d.ts.map +1 -1
  24. package/dist/core/garbageCollection.d.ts +2 -2
  25. package/dist/core/garbageCollection.d.ts.map +1 -1
  26. package/dist/core/logging.d.ts +13 -4
  27. package/dist/core/logging.d.ts.map +1 -1
  28. package/dist/core/makeNetworkRequest.d.ts +3 -3
  29. package/dist/core/makeNetworkRequest.d.ts.map +1 -1
  30. package/dist/core/makeNetworkRequest.js +15 -15
  31. package/dist/core/read.d.ts +8 -8
  32. package/dist/core/read.d.ts.map +1 -1
  33. package/dist/core/read.js +98 -29
  34. package/dist/core/reader.d.ts +12 -8
  35. package/dist/core/reader.d.ts.map +1 -1
  36. package/dist/core/startUpdate.d.ts +7 -4
  37. package/dist/core/startUpdate.d.ts.map +1 -1
  38. package/dist/core/startUpdate.js +153 -5
  39. package/dist/index.d.ts +8 -5
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +8 -1
  42. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -1
  43. package/dist/loadable-hooks/useConnectionSpecPagination.js +1 -1
  44. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -1
  45. package/dist/loadable-hooks/useSkipLimitPagination.js +1 -1
  46. package/dist/react/FragmentReader.d.ts +7 -13
  47. package/dist/react/FragmentReader.d.ts.map +1 -1
  48. package/dist/react/FragmentReader.js +3 -30
  49. package/dist/react/FragmentRenderer.d.ts +15 -0
  50. package/dist/react/FragmentRenderer.d.ts.map +1 -0
  51. package/dist/react/FragmentRenderer.js +35 -0
  52. package/dist/react/LoadableFieldReader.d.ts +12 -0
  53. package/dist/react/LoadableFieldReader.d.ts.map +1 -0
  54. package/dist/react/LoadableFieldReader.js +10 -0
  55. package/dist/react/LoadableFieldRenderer.d.ts +13 -0
  56. package/dist/react/LoadableFieldRenderer.d.ts.map +1 -0
  57. package/dist/react/LoadableFieldRenderer.js +37 -0
  58. package/dist/react/useImperativeReference.d.ts.map +1 -1
  59. package/dist/react/useImperativeReference.js +6 -6
  60. package/dist/react/useResult.d.ts.map +1 -1
  61. package/dist/react/useResult.js +1 -1
  62. package/package.json +4 -5
  63. package/src/core/FragmentReference.ts +16 -7
  64. package/src/core/IsographEnvironment.ts +19 -9
  65. package/src/core/PromiseWrapper.ts +13 -16
  66. package/src/core/areEqualWithDeepComparison.ts +5 -5
  67. package/src/core/brand.ts +18 -0
  68. package/src/core/cache.ts +50 -43
  69. package/src/core/check.ts +4 -4
  70. package/src/core/componentCache.ts +5 -1
  71. package/src/core/entrypoint.ts +32 -5
  72. package/src/core/garbageCollection.ts +3 -3
  73. package/src/core/logging.ts +16 -4
  74. package/src/core/makeNetworkRequest.ts +48 -23
  75. package/src/core/read.ts +153 -48
  76. package/src/core/reader.ts +11 -7
  77. package/src/core/startUpdate.ts +313 -7
  78. package/src/index.ts +11 -3
  79. package/src/loadable-hooks/useConnectionSpecPagination.ts +1 -0
  80. package/src/loadable-hooks/useSkipLimitPagination.ts +1 -0
  81. package/src/react/FragmentReader.tsx +23 -39
  82. package/src/react/FragmentRenderer.tsx +46 -0
  83. package/src/react/LoadableFieldReader.tsx +40 -0
  84. package/src/react/LoadableFieldRenderer.tsx +41 -0
  85. package/src/react/useImperativeReference.ts +9 -8
  86. package/src/react/useResult.ts +1 -0
  87. package/src/tests/__isograph/Economist/link/output_type.ts +2 -0
  88. package/src/tests/__isograph/Node/asEconomist/resolver_reader.ts +28 -0
  89. package/src/tests/__isograph/Node/link/output_type.ts +3 -0
  90. package/src/tests/__isograph/Query/linkedUpdate/entrypoint.ts +31 -0
  91. package/src/tests/__isograph/Query/linkedUpdate/normalization_ast.ts +95 -0
  92. package/src/tests/__isograph/Query/linkedUpdate/output_type.ts +3 -0
  93. package/src/tests/__isograph/Query/linkedUpdate/param_type.ts +51 -0
  94. package/src/tests/__isograph/Query/linkedUpdate/query_text.ts +20 -0
  95. package/src/tests/__isograph/Query/linkedUpdate/resolver_reader.ts +93 -0
  96. package/src/tests/__isograph/Query/meName/entrypoint.ts +4 -1
  97. package/src/tests/__isograph/Query/meName/query_text.ts +1 -1
  98. package/src/tests/__isograph/Query/meName/resolver_reader.ts +1 -0
  99. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +4 -1
  100. package/src/tests/__isograph/Query/meNameSuccessor/query_text.ts +1 -1
  101. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +3 -0
  102. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +4 -1
  103. package/src/tests/__isograph/Query/nodeField/query_text.ts +1 -1
  104. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +1 -0
  105. package/src/tests/__isograph/Query/startUpdate/entrypoint.ts +31 -0
  106. package/src/tests/__isograph/Query/startUpdate/normalization_ast.ts +51 -0
  107. package/src/tests/__isograph/Query/startUpdate/output_type.ts +3 -0
  108. package/src/tests/__isograph/Query/startUpdate/param_type.ts +26 -0
  109. package/src/tests/__isograph/Query/startUpdate/parameters_type.ts +3 -0
  110. package/src/tests/__isograph/Query/startUpdate/query_text.ts +11 -0
  111. package/src/tests/__isograph/Query/startUpdate/resolver_reader.ts +55 -0
  112. package/src/tests/__isograph/Query/subquery/entrypoint.ts +4 -1
  113. package/src/tests/__isograph/Query/subquery/query_text.ts +1 -1
  114. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +2 -0
  115. package/src/tests/__isograph/iso.ts +21 -1
  116. package/src/tests/__isograph/tsconfig.json +8 -0
  117. package/src/tests/normalizeData.test.ts +0 -1
  118. package/src/tests/startUpdate.test.ts +205 -0
  119. package/.turbo/turbo-compile-typescript.log +0 -4
@@ -1,6 +1,6 @@
1
1
  import { ReaderWithRefetchQueries } from '../core/entrypoint';
2
2
  import { stableCopy } from './cache';
3
- import { type Link } from './IsographEnvironment';
3
+ import { type StoreLink } from './IsographEnvironment';
4
4
  import { PromiseWrapper } from './PromiseWrapper';
5
5
  import type { StartUpdate } from './reader';
6
6
 
@@ -35,11 +35,14 @@ export type ExtractParameters<T> = T extends {
35
35
  ? P
36
36
  : Variables;
37
37
 
38
- export type ExtractStartUpdate<
39
- T extends {
40
- startUpdate?: StartUpdate<object>;
41
- },
42
- > = T['startUpdate'];
38
+ export type ExtractStartUpdate<T extends UnknownTReadFromStore> =
39
+ T['startUpdate'];
40
+
41
+ export type ExtractUpdatableData<T extends UnknownTReadFromStore> =
42
+ ExtractUpdatableDataFromStartUpdate<ExtractStartUpdate<T>>;
43
+
44
+ export type ExtractUpdatableDataFromStartUpdate<T> =
45
+ T extends StartUpdate<infer D> ? D : never;
43
46
 
44
47
  export type FragmentReference<
45
48
  TReadFromStore extends UnknownTReadFromStore,
@@ -49,7 +52,13 @@ export type FragmentReference<
49
52
  readonly readerWithRefetchQueries: PromiseWrapper<
50
53
  ReaderWithRefetchQueries<TReadFromStore, TClientFieldValue>
51
54
  >;
52
- readonly root: Link;
55
+ readonly root: StoreLink;
56
+ // TODO we potentially stably copy and stringify variables a lot!
57
+ // So, we should employ interior mutability: pretend that fragent reference
58
+ // is immutable, but actually store something like
59
+ // `Map<Variable, StablyCopiedStringifiedOutput>`
60
+ // and read or update that map when we would otherwise stably copy and
61
+ // stringify.
53
62
  readonly variables: ExtractParameters<TReadFromStore>;
54
63
  readonly networkRequest: PromiseWrapper<void, any>;
55
64
  };
@@ -1,5 +1,9 @@
1
1
  import { ParentCache } from '@isograph/react-disposable-state';
2
- import { IsographEntrypoint } from './entrypoint';
2
+ import {
3
+ IsographEntrypoint,
4
+ IsographOperation,
5
+ IsographPersistedOperation,
6
+ } from './entrypoint';
3
7
  import {
4
8
  FragmentReference,
5
9
  Variables,
@@ -11,6 +15,7 @@ import { LogFunction, WrappedLogFunction } from './logging';
11
15
  import { PromiseWrapper, wrapPromise } from './PromiseWrapper';
12
16
  import { WithEncounteredRecords } from './read';
13
17
  import type { ReaderAst, StartUpdate } from './reader';
18
+ import type { Brand } from './brand';
14
19
 
15
20
  export type ComponentOrFieldName = string;
16
21
  export type StringifiedArgs = string;
@@ -34,7 +39,7 @@ export type FragmentSubscription<TReadFromStore extends UnknownTReadFromStore> =
34
39
  export type AnyChangesToRecordSubscription = {
35
40
  readonly kind: 'AnyChangesToRecord';
36
41
  readonly callback: () => void;
37
- readonly recordLink: Link;
42
+ readonly recordLink: StoreLink;
38
43
  };
39
44
 
40
45
  export type AnyRecordSubscription = {
@@ -73,18 +78,23 @@ export type IsographEnvironment = {
73
78
 
74
79
  export type MissingFieldHandler = (
75
80
  storeRecord: StoreRecord,
76
- root: Link,
81
+ root: StoreLink,
77
82
  fieldName: string,
78
83
  arguments_: { [index: string]: any } | null,
79
84
  variables: Variables | null,
80
- ) => Link | undefined;
85
+ ) => StoreLink | undefined;
81
86
 
82
87
  export type IsographNetworkFunction = (
83
- queryText: string,
88
+ operation: IsographOperation | IsographPersistedOperation,
84
89
  variables: Variables,
85
90
  ) => Promise<any>;
86
91
 
87
- export type Link = {
92
+ export interface Link<T extends TypeName> extends StoreLink {
93
+ readonly __link: Brand<DataId, T>;
94
+ readonly __typename: T;
95
+ }
96
+
97
+ export type StoreLink = {
88
98
  readonly __link: DataId;
89
99
  readonly __typename: TypeName;
90
100
  };
@@ -99,7 +109,7 @@ export type DataTypeValue =
99
109
  | string
100
110
  | null
101
111
  // Singular linked fields:
102
- | Link
112
+ | StoreLink
103
113
  // Plural scalar and linked fields:
104
114
  | DataTypeValue[];
105
115
 
@@ -158,7 +168,7 @@ export function createIsographStore(): IsographStore {
158
168
  };
159
169
  }
160
170
 
161
- export function assertLink(link: DataTypeValue): Link | null | undefined {
171
+ export function assertLink(link: DataTypeValue): StoreLink | null | undefined {
162
172
  if (Array.isArray(link)) {
163
173
  throw new Error('Unexpected array');
164
174
  }
@@ -171,7 +181,7 @@ export function assertLink(link: DataTypeValue): Link | null | undefined {
171
181
  throw new Error('Invalid link');
172
182
  }
173
183
 
174
- export function getLink(maybeLink: DataTypeValue): Link | null {
184
+ export function getLink(maybeLink: DataTypeValue): StoreLink | null {
175
185
  if (
176
186
  maybeLink != null &&
177
187
  typeof maybeLink === 'object' &&
@@ -1,6 +1,6 @@
1
1
  export type AnyError = any;
2
2
 
3
- export const NOT_SET: Symbol = Symbol('NOT_SET');
3
+ export const NOT_SET = Symbol('NOT_SET');
4
4
  export type NotSet = typeof NOT_SET;
5
5
 
6
6
  export type Result<T, E> =
@@ -18,31 +18,32 @@ export type Result<T, E> =
18
18
  * Before the promise is resolved, value becomes non-null.
19
19
  */
20
20
  export type PromiseWrapper<T, E = any> = {
21
- readonly promise: Promise<T>;
21
+ readonly promise: Promise<Exclude<T, NotSet>>;
22
22
  result: Result<Exclude<T, NotSet>, E> | NotSet;
23
23
  };
24
24
 
25
- export function wrapPromise<T>(promise: Promise<T>): PromiseWrapper<T, any> {
25
+ export function wrapPromise<T>(
26
+ promise: Promise<Exclude<T, NotSet>>,
27
+ ): PromiseWrapper<T, unknown> {
26
28
  // TODO confirm suspense works if the promise is already resolved.
27
29
  const wrapper: PromiseWrapper<T, any> = { promise, result: NOT_SET };
28
30
  promise
29
31
  .then((v) => {
30
- // v is assignable to Exclude<T, Symbol> | Symbol
31
- wrapper.result = { kind: 'Ok', value: v as any };
32
+ wrapper.result = { kind: 'Ok', value: v };
32
33
  })
33
34
  .catch((e) => {
34
- // e is assignable to Exclude<T, Symbol> | Symbol
35
- wrapper.result = { kind: 'Err', error: e as any };
35
+ wrapper.result = { kind: 'Err', error: e };
36
36
  });
37
37
  return wrapper;
38
38
  }
39
39
 
40
- export function wrapResolvedValue<T>(value: T): PromiseWrapper<T, never> {
40
+ export function wrapResolvedValue<T>(
41
+ value: Exclude<T, NotSet>,
42
+ ): PromiseWrapper<T, never> {
41
43
  return {
42
44
  promise: Promise.resolve(value),
43
45
  result: {
44
46
  kind: 'Ok',
45
- // @ts-expect-error one should not call wrapResolvedValue with NOT_SET
46
47
  value,
47
48
  },
48
49
  };
@@ -51,8 +52,7 @@ export function wrapResolvedValue<T>(value: T): PromiseWrapper<T, never> {
51
52
  export function readPromise<T, E>(p: PromiseWrapper<T, E>): T {
52
53
  const { result } = p;
53
54
  if (result !== NOT_SET) {
54
- // Safety: p.result is either NOT_SET or an actual value.
55
- const resultKind = result as Result<T, any>;
55
+ const resultKind = result;
56
56
  if (resultKind.kind === 'Ok') {
57
57
  return resultKind.value;
58
58
  } else {
@@ -73,11 +73,8 @@ export type PromiseState<T, E> =
73
73
  export function getPromiseState<T, E>(
74
74
  p: PromiseWrapper<T, E>,
75
75
  ): PromiseState<T, E> {
76
- const { result } = p;
77
- if (result !== NOT_SET) {
78
- // Safety: p.result is either NOT_SET or an actual value.
79
- const resultKind = result as Result<T, any>;
80
- return resultKind;
76
+ if (p.result !== NOT_SET) {
77
+ return p.result;
81
78
  }
82
79
  return {
83
80
  kind: 'Pending',
@@ -1,7 +1,7 @@
1
- import type { Link } from './IsographEnvironment';
1
+ import type { StoreLink } from './IsographEnvironment';
2
2
  import type { ReaderAst, ReaderLinkedField, ReaderScalarField } from './reader';
3
3
 
4
- export function mergeUsingReaderAst(
4
+ function mergeUsingReaderAst(
5
5
  field: ReaderScalarField | ReaderLinkedField,
6
6
  oldItem: unknown,
7
7
  newItem: unknown,
@@ -40,7 +40,7 @@ export function mergeUsingReaderAst(
40
40
  }
41
41
  }
42
42
 
43
- export function mergeArraysUsingReaderAst(
43
+ function mergeArraysUsingReaderAst(
44
44
  field: ReaderScalarField | ReaderLinkedField,
45
45
  oldItems: ReadonlyArray<unknown>,
46
46
  newItems: Array<unknown>,
@@ -101,9 +101,9 @@ export function mergeObjectsUsingReaderAst(
101
101
  case 'Link': {
102
102
  const key = field.alias;
103
103
  // @ts-expect-error
104
- const oldValue: Link = oldItemObject[key];
104
+ const oldValue: StoreLink = oldItemObject[key];
105
105
  // @ts-expect-error
106
- const newValue: Link = newItemObject[key];
106
+ const newValue: StoreLink = newItemObject[key];
107
107
 
108
108
  if (
109
109
  oldValue.__link !== newValue.__link ||
@@ -0,0 +1,18 @@
1
+ // Suppress the TypeScript compiler warning for this branded-type trick.
2
+ // See discussion: https://github.com/microsoft/TypeScript/issues/202#issuecomment-436900738
3
+ // Pattern: “Brand<BaseType, Brand>” leverages TypeScript conditional and `infer` types to create a pseudo-nominal type,
4
+ // enabling BaseType to be treated as distinct only when tagged with Brand, even though Brand doesn't exist at runtime.
5
+ //
6
+ // Explanation:
7
+ // - Brand extends `symbol | string` acts as a “brand” identifier.
8
+ // - The type uses a conditional check `infer _ extends Brand ? BaseType : never` to strip out BaseType when the branding doesn't match.
9
+ // - This yields a branded type system: `Brand<string, "UserId">` is not accidentally assignable to `Brand<string, "ProductId">`.
10
+ //
11
+ // Usage: Helps enforce semantic distinctions (e.g., distinguishing user IDs from product IDs) even when their runtime values are both strings.
12
+ //
13
+ // Caveat: This is purely a compile-time trick—Brand is erased in emitted JavaScript, so runtime checks must rely on other mechanisms.
14
+ export type Brand<
15
+ BaseType,
16
+ Brand extends symbol | string,
17
+ // @ts-ignore
18
+ > = infer _ extends Brand ? BaseType : never;
package/src/core/cache.ts CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  NormalizationInlineFragment,
9
9
  NormalizationLinkedField,
10
10
  NormalizationScalarField,
11
- RefetchQueryNormalizationArtifactWrapper,
12
11
  type NormalizationAst,
13
12
  type NormalizationAstLoader,
14
13
  type NormalizationAstNodes,
@@ -27,15 +26,15 @@ import {
27
26
  DataTypeValue,
28
27
  FragmentSubscription,
29
28
  getLink,
30
- Link,
31
29
  ROOT_ID,
30
+ StoreLink,
32
31
  StoreRecord,
33
32
  type IsographEnvironment,
34
33
  type TypeName,
35
34
  } from './IsographEnvironment';
36
35
  import { logMessage } from './logging';
37
36
  import { maybeMakeNetworkRequest } from './makeNetworkRequest';
38
- import { wrapResolvedValue } from './PromiseWrapper';
37
+ import { wrapPromise, wrapResolvedValue } from './PromiseWrapper';
39
38
  import { readButDoNotEvaluate, WithEncounteredRecords } from './read';
40
39
  import { ReaderLinkedField, ReaderScalarField, type ReaderAst } from './reader';
41
40
  import { Argument, ArgumentValue } from './util';
@@ -93,15 +92,31 @@ export function getOrCreateCacheForArtifact<
93
92
  variables: ExtractParameters<TReadFromStore>,
94
93
  fetchOptions?: FetchOptions<TClientFieldValue>,
95
94
  ): ParentCache<FragmentReference<TReadFromStore, TClientFieldValue>> {
96
- const cacheKey =
97
- entrypoint.networkRequestInfo.queryText +
98
- JSON.stringify(stableCopy(variables));
95
+ let cacheKey = '';
96
+ switch (entrypoint.networkRequestInfo.operation.kind) {
97
+ case 'Operation':
98
+ cacheKey =
99
+ entrypoint.networkRequestInfo.operation.text +
100
+ JSON.stringify(stableCopy(variables));
101
+ break;
102
+ case 'PersistedOperation':
103
+ cacheKey =
104
+ entrypoint.networkRequestInfo.operation.operationId +
105
+ JSON.stringify(stableCopy(variables));
106
+ break;
107
+ }
99
108
  const factory = () => {
109
+ const readerWithRefetchQueries =
110
+ entrypoint.readerWithRefetchQueries.kind ===
111
+ 'ReaderWithRefetchQueriesLoader'
112
+ ? wrapPromise(entrypoint.readerWithRefetchQueries.loader())
113
+ : wrapResolvedValue(entrypoint.readerWithRefetchQueries);
100
114
  const [networkRequest, disposeNetworkRequest] = maybeMakeNetworkRequest(
101
115
  environment,
102
116
  entrypoint,
103
117
  variables,
104
- fetchOptions,
118
+ readerWithRefetchQueries,
119
+ fetchOptions ?? null,
105
120
  );
106
121
 
107
122
  const itemCleanupPair: ItemCleanupPair<
@@ -109,12 +124,7 @@ export function getOrCreateCacheForArtifact<
109
124
  > = [
110
125
  {
111
126
  kind: 'FragmentReference',
112
- readerWithRefetchQueries: wrapResolvedValue({
113
- kind: 'ReaderWithRefetchQueries',
114
- readerArtifact: entrypoint.readerWithRefetchQueries.readerArtifact,
115
- nestedRefetchQueries:
116
- entrypoint.readerWithRefetchQueries.nestedRefetchQueries,
117
- }),
127
+ readerWithRefetchQueries,
118
128
  root: { __link: ROOT_ID, __typename: entrypoint.concreteType },
119
129
  variables,
120
130
  networkRequest: networkRequest,
@@ -147,8 +157,7 @@ export function normalizeData(
147
157
  normalizationAst: NormalizationAstNodes,
148
158
  networkResponse: NetworkResponseObject,
149
159
  variables: Variables,
150
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
151
- root: Link,
160
+ root: StoreLink,
152
161
  ): EncounteredIds {
153
162
  const encounteredIds: EncounteredIds = new Map();
154
163
 
@@ -169,7 +178,6 @@ export function normalizeData(
169
178
  newStoreRecord,
170
179
  root,
171
180
  variables,
172
- nestedRefetchQueries,
173
181
  encounteredIds,
174
182
  );
175
183
 
@@ -197,7 +205,7 @@ export function subscribeToAnyChange(
197
205
 
198
206
  export function subscribeToAnyChangesToRecord(
199
207
  environment: IsographEnvironment,
200
- recordLink: Link,
208
+ recordLink: StoreLink,
201
209
  callback: () => void,
202
210
  ): () => void {
203
211
  const subscription = {
@@ -232,7 +240,7 @@ export function subscribe<TReadFromStore extends UnknownTReadFromStore>(
232
240
 
233
241
  export function onNextChangeToRecord(
234
242
  environment: IsographEnvironment,
235
- recordLink: Link,
243
+ recordLink: StoreLink,
236
244
  ): Promise<void> {
237
245
  return new Promise((resolve) => {
238
246
  const unsubscribe = subscribeToAnyChangesToRecord(
@@ -251,20 +259,28 @@ export function onNextChangeToRecord(
251
259
  //
252
260
  // That's probably okay to ignore. We don't, however, want to prevent
253
261
  // updating other subscriptions if one subscription had missing data.
254
- function withErrorHandling<T>(f: (t: T) => void): (t: T) => void {
262
+ function withErrorHandling<T>(
263
+ environment: IsographEnvironment,
264
+ f: (t: T) => void,
265
+ ): (t: T) => void {
255
266
  return (t) => {
256
267
  try {
257
268
  return f(t);
258
- } catch {}
269
+ } catch (e) {
270
+ logMessage(environment, () => ({
271
+ kind: 'ErrorEncounteredInWithErrorHandling',
272
+ error: e,
273
+ }));
274
+ }
259
275
  };
260
276
  }
261
277
 
262
- function callSubscriptions(
278
+ export function callSubscriptions(
263
279
  environment: IsographEnvironment,
264
280
  recordsEncounteredWhenNormalizing: EncounteredIds,
265
281
  ) {
266
282
  environment.subscriptions.forEach(
267
- withErrorHandling((subscription) => {
283
+ withErrorHandling(environment, (subscription) => {
268
284
  switch (subscription.kind) {
269
285
  case 'FragmentSubscription': {
270
286
  // TODO if there are multiple components subscribed to the same
@@ -379,9 +395,8 @@ function normalizeDataIntoRecord(
379
395
  normalizationAst: NormalizationAstNodes,
380
396
  networkResponseParentRecord: NetworkResponseObject,
381
397
  targetParentRecord: StoreRecord,
382
- targetParentRecordLink: Link,
398
+ targetParentRecordLink: StoreLink,
383
399
  variables: Variables,
384
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
385
400
  mutableEncounteredIds: EncounteredIds,
386
401
  ): RecordHasBeenUpdated {
387
402
  let recordHasBeenUpdated = false;
@@ -406,7 +421,6 @@ function normalizeDataIntoRecord(
406
421
  targetParentRecord,
407
422
  targetParentRecordLink,
408
423
  variables,
409
- nestedRefetchQueries,
410
424
  mutableEncounteredIds,
411
425
  );
412
426
  recordHasBeenUpdated =
@@ -421,7 +435,6 @@ function normalizeDataIntoRecord(
421
435
  targetParentRecord,
422
436
  targetParentRecordLink,
423
437
  variables,
424
- nestedRefetchQueries,
425
438
  mutableEncounteredIds,
426
439
  );
427
440
  recordHasBeenUpdated =
@@ -437,7 +450,7 @@ function normalizeDataIntoRecord(
437
450
  }
438
451
  }
439
452
  if (recordHasBeenUpdated) {
440
- let encounteredRecordsIds = insertIfNotExists(
453
+ let encounteredRecordsIds = insertEmptySetIfMissing(
441
454
  mutableEncounteredIds,
442
455
  targetParentRecordLink.__typename,
443
456
  );
@@ -447,7 +460,7 @@ function normalizeDataIntoRecord(
447
460
  return recordHasBeenUpdated;
448
461
  }
449
462
 
450
- export function insertIfNotExists<K, V>(map: Map<K, Set<V>>, key: K) {
463
+ export function insertEmptySetIfMissing<K, V>(map: Map<K, Set<V>>, key: K) {
451
464
  let result = map.get(key);
452
465
  if (result === undefined) {
453
466
  result = new Set();
@@ -487,9 +500,8 @@ function normalizeLinkedField(
487
500
  astNode: NormalizationLinkedField,
488
501
  networkResponseParentRecord: NetworkResponseObject,
489
502
  targetParentRecord: StoreRecord,
490
- targetParentRecordLink: Link,
503
+ targetParentRecordLink: StoreLink,
491
504
  variables: Variables,
492
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
493
505
  mutableEncounteredIds: EncounteredIds,
494
506
  ): RecordHasBeenUpdated {
495
507
  const networkResponseKey = getNetworkResponseKey(astNode);
@@ -513,7 +525,7 @@ function normalizeLinkedField(
513
525
 
514
526
  if (Array.isArray(networkResponseData)) {
515
527
  // TODO check astNode.plural or the like
516
- const dataIds: (Link | null)[] = [];
528
+ const dataIds: (StoreLink | null)[] = [];
517
529
  for (let i = 0; i < networkResponseData.length; i++) {
518
530
  const networkResponseObject = networkResponseData[i];
519
531
  if (networkResponseObject == null) {
@@ -527,7 +539,6 @@ function normalizeLinkedField(
527
539
  targetParentRecordLink,
528
540
  variables,
529
541
  i,
530
- nestedRefetchQueries,
531
542
  mutableEncounteredIds,
532
543
  );
533
544
 
@@ -554,7 +565,6 @@ function normalizeLinkedField(
554
565
  targetParentRecordLink,
555
566
  variables,
556
567
  null,
557
- nestedRefetchQueries,
558
568
  mutableEncounteredIds,
559
569
  );
560
570
 
@@ -586,9 +596,8 @@ function normalizeInlineFragment(
586
596
  astNode: NormalizationInlineFragment,
587
597
  networkResponseParentRecord: NetworkResponseObject,
588
598
  targetParentRecord: StoreRecord,
589
- targetParentRecordLink: Link,
599
+ targetParentRecordLink: StoreLink,
590
600
  variables: Variables,
591
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
592
601
  mutableEncounteredIds: EncounteredIds,
593
602
  ): RecordHasBeenUpdated {
594
603
  const typeToRefineTo = astNode.type;
@@ -600,7 +609,6 @@ function normalizeInlineFragment(
600
609
  targetParentRecord,
601
610
  targetParentRecordLink,
602
611
  variables,
603
- nestedRefetchQueries,
604
612
  mutableEncounteredIds,
605
613
  );
606
614
  return hasBeenModified;
@@ -610,7 +618,7 @@ function normalizeInlineFragment(
610
618
 
611
619
  function dataIdsAreTheSame(
612
620
  existingValue: DataTypeValue,
613
- newDataIds: (Link | null)[],
621
+ newDataIds: (StoreLink | null)[],
614
622
  ): boolean {
615
623
  if (Array.isArray(existingValue)) {
616
624
  if (newDataIds.length !== existingValue.length) {
@@ -635,10 +643,9 @@ function normalizeNetworkResponseObject(
635
643
  environment: IsographEnvironment,
636
644
  astNode: NormalizationLinkedField,
637
645
  networkResponseData: NetworkResponseObject,
638
- targetParentRecordLink: Link,
646
+ targetParentRecordLink: StoreLink,
639
647
  variables: Variables,
640
648
  index: number | null,
641
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
642
649
  mutableEncounteredIds: EncounteredIds,
643
650
  ): DataId /* The id of the modified or newly created item */ {
644
651
  const newStoreRecordId = getDataIdOfNetworkResponse(
@@ -668,7 +675,6 @@ function normalizeNetworkResponseObject(
668
675
  newStoreRecord,
669
676
  { __link: newStoreRecordId, __typename: __typename },
670
677
  variables,
671
- nestedRefetchQueries,
672
678
  mutableEncounteredIds,
673
679
  );
674
680
 
@@ -760,13 +766,14 @@ function getStoreKeyChunkForArgumentValue(
760
766
  }
761
767
 
762
768
  function getStoreKeyChunkForArgument(argument: Argument, variables: Variables) {
763
- let chunk = getStoreKeyChunkForArgumentValue(argument[1], variables);
769
+ const [argumentName, argumentValue] = argument;
770
+ let chunk = getStoreKeyChunkForArgumentValue(argumentValue, variables);
764
771
 
765
772
  if (typeof chunk === 'object') {
766
773
  chunk = JSON.stringify(stableCopy(chunk));
767
774
  }
768
775
 
769
- return `${FIRST_SPLIT_KEY}${argument[0]}${SECOND_SPLIT_KEY}${chunk}`;
776
+ return `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${chunk}`;
770
777
  }
771
778
 
772
779
  function getNetworkResponseKey(
@@ -831,7 +838,7 @@ export const THIRD_SPLIT_KEY = '__';
831
838
 
832
839
  // Returns a key to look up an item in the store
833
840
  function getDataIdOfNetworkResponse(
834
- parentRecordLink: Link,
841
+ parentRecordLink: StoreLink,
835
842
  dataToNormalize: NetworkResponseObject,
836
843
  astNode: NormalizationLinkedField,
837
844
  variables: Variables,
package/src/core/check.ts CHANGED
@@ -4,7 +4,7 @@ import { Variables } from './FragmentReference';
4
4
  import {
5
5
  getLink,
6
6
  IsographEnvironment,
7
- Link,
7
+ StoreLink,
8
8
  StoreRecord,
9
9
  } from './IsographEnvironment';
10
10
  import { logMessage } from './logging';
@@ -30,14 +30,14 @@ export type CheckResult =
30
30
  }
31
31
  | {
32
32
  kind: 'MissingData';
33
- record: Link;
33
+ record: StoreLink;
34
34
  };
35
35
 
36
36
  export function check(
37
37
  environment: IsographEnvironment,
38
38
  normalizationAst: NormalizationAstNodes,
39
39
  variables: Variables,
40
- root: Link,
40
+ root: StoreLink,
41
41
  ): CheckResult {
42
42
  const recordsById = (environment.store[root.__typename] ??= {});
43
43
  const newStoreRecord = (recordsById[root.__link] ??= {});
@@ -61,7 +61,7 @@ function checkFromRecord(
61
61
  normalizationAst: NormalizationAstNodes,
62
62
  variables: Variables,
63
63
  record: StoreRecord,
64
- recordLink: Link,
64
+ recordLink: StoreLink,
65
65
  ): CheckResult {
66
66
  normalizationAstLoop: for (const normalizationAstNode of normalizationAst) {
67
67
  switch (normalizationAstNode.kind) {
@@ -16,7 +16,11 @@ export function getOrCreateCachedComponent(
16
16
  networkRequestOptions: NetworkRequestReaderOptions,
17
17
  ): React.FC<any> {
18
18
  // We create startUpdate outside of component to make it stable
19
- const startUpdate = createStartUpdate(environment, fragmentReference);
19
+ const startUpdate = createStartUpdate(
20
+ environment,
21
+ fragmentReference,
22
+ networkRequestOptions,
23
+ );
20
24
 
21
25
  return (environment.componentCache[
22
26
  stableIdForFragmentReference(fragmentReference, componentName)
@@ -17,11 +17,39 @@ export type ReaderWithRefetchQueries<
17
17
  readonly nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[];
18
18
  };
19
19
 
20
+ export type ReaderWithRefetchQueriesLoader<
21
+ TReadFromStore extends UnknownTReadFromStore,
22
+ TClientFieldValue,
23
+ > = {
24
+ readonly kind: 'ReaderWithRefetchQueriesLoader';
25
+ readonly loader: () => Promise<
26
+ ReaderWithRefetchQueries<TReadFromStore, TClientFieldValue>
27
+ >;
28
+ };
29
+
20
30
  export type NetworkRequestInfo<TNormalizationAst> = {
21
31
  readonly kind: 'NetworkRequestInfo';
22
- readonly queryText: string;
32
+ readonly operation: IsographOperation | IsographPersistedOperation;
23
33
  readonly normalizationAst: TNormalizationAst;
24
34
  };
35
+
36
+ export type IsographOperation = {
37
+ readonly kind: 'Operation';
38
+ readonly text: string;
39
+ };
40
+
41
+ export type IsographPersistedOperation = {
42
+ readonly kind: 'PersistedOperation';
43
+ readonly operationId: string;
44
+ readonly extraInfo: IsographPersistedOperationExtraInfo | null;
45
+ };
46
+
47
+ export type IsographPersistedOperationExtraInfo = {
48
+ readonly kind: 'PersistedOperationExtraInfo';
49
+ readonly operationName: string | null;
50
+ readonly operationKind: 'Query' | 'Mutation' | 'Subscription';
51
+ };
52
+
25
53
  // This type should be treated as an opaque type.
26
54
  export type IsographEntrypoint<
27
55
  TReadFromStore extends UnknownTReadFromStore,
@@ -30,10 +58,9 @@ export type IsographEntrypoint<
30
58
  > = {
31
59
  readonly kind: 'Entrypoint';
32
60
  readonly networkRequestInfo: NetworkRequestInfo<TNormalizationAst>;
33
- readonly readerWithRefetchQueries: ReaderWithRefetchQueries<
34
- TReadFromStore,
35
- TClientFieldValue
36
- >;
61
+ readonly readerWithRefetchQueries:
62
+ | ReaderWithRefetchQueries<TReadFromStore, TClientFieldValue>
63
+ | ReaderWithRefetchQueriesLoader<TReadFromStore, TClientFieldValue>;
37
64
  readonly concreteType: TypeName;
38
65
  };
39
66
 
@@ -7,14 +7,14 @@ import {
7
7
  IsographEnvironment,
8
8
  IsographStore,
9
9
  StoreRecord,
10
- type Link,
10
+ type StoreLink,
11
11
  type TypeName,
12
12
  } from './IsographEnvironment';
13
13
 
14
14
  export type RetainedQuery = {
15
15
  readonly normalizationAst: NormalizationAstNodes;
16
16
  readonly variables: {};
17
- readonly root: Link;
17
+ readonly root: StoreLink;
18
18
  };
19
19
 
20
20
  export type DidUnretainSomeQuery = boolean;
@@ -117,7 +117,7 @@ function recordReachableIdsFromRecord(
117
117
  const linkKey = getParentRecordKey(selection, variables ?? {});
118
118
  const linkedFieldOrFields = currentRecord[linkKey];
119
119
 
120
- const links: Link[] = [];
120
+ const links: StoreLink[] = [];
121
121
  if (Array.isArray(linkedFieldOrFields)) {
122
122
  for (const maybeLink of linkedFieldOrFields) {
123
123
  const link = assertLink(maybeLink);