@isograph/react 0.3.1 → 0.4.1

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 (130) 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 +15 -10
  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 +57 -36
  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 +3 -2
  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/core/util.d.ts +2 -1
  40. package/dist/core/util.d.ts.map +1 -1
  41. package/dist/index.d.ts +8 -5
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -1
  44. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -1
  45. package/dist/loadable-hooks/useConnectionSpecPagination.js +1 -1
  46. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +1 -1
  47. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts.map +1 -1
  48. package/dist/loadable-hooks/useImperativeExposedMutationField.js +1 -1
  49. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +1 -1
  50. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -1
  51. package/dist/loadable-hooks/useImperativeLoadableField.js +1 -1
  52. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -1
  53. package/dist/loadable-hooks/useSkipLimitPagination.js +1 -1
  54. package/dist/react/FragmentReader.d.ts +7 -13
  55. package/dist/react/FragmentReader.d.ts.map +1 -1
  56. package/dist/react/FragmentReader.js +3 -30
  57. package/dist/react/FragmentRenderer.d.ts +15 -0
  58. package/dist/react/FragmentRenderer.d.ts.map +1 -0
  59. package/dist/react/FragmentRenderer.js +35 -0
  60. package/dist/react/LoadableFieldReader.d.ts +12 -0
  61. package/dist/react/LoadableFieldReader.d.ts.map +1 -0
  62. package/dist/react/LoadableFieldReader.js +10 -0
  63. package/dist/react/LoadableFieldRenderer.d.ts +13 -0
  64. package/dist/react/LoadableFieldRenderer.d.ts.map +1 -0
  65. package/dist/react/LoadableFieldRenderer.js +37 -0
  66. package/dist/react/useImperativeReference.d.ts.map +1 -1
  67. package/dist/react/useImperativeReference.js +6 -6
  68. package/dist/react/useResult.d.ts.map +1 -1
  69. package/dist/react/useResult.js +1 -1
  70. package/package.json +4 -5
  71. package/src/core/FragmentReference.ts +16 -7
  72. package/src/core/IsographEnvironment.ts +20 -10
  73. package/src/core/PromiseWrapper.ts +13 -16
  74. package/src/core/areEqualWithDeepComparison.ts +5 -5
  75. package/src/core/brand.ts +18 -0
  76. package/src/core/cache.ts +74 -51
  77. package/src/core/check.ts +4 -4
  78. package/src/core/componentCache.ts +7 -2
  79. package/src/core/entrypoint.ts +32 -5
  80. package/src/core/garbageCollection.ts +3 -3
  81. package/src/core/logging.ts +16 -4
  82. package/src/core/makeNetworkRequest.ts +48 -23
  83. package/src/core/read.ts +153 -48
  84. package/src/core/reader.ts +11 -7
  85. package/src/core/startUpdate.ts +313 -7
  86. package/src/core/util.ts +4 -2
  87. package/src/index.ts +11 -3
  88. package/src/loadable-hooks/useConnectionSpecPagination.ts +1 -0
  89. package/src/loadable-hooks/useImperativeExposedMutationField.ts +2 -2
  90. package/src/loadable-hooks/useImperativeLoadableField.ts +2 -2
  91. package/src/loadable-hooks/useSkipLimitPagination.ts +1 -0
  92. package/src/react/FragmentReader.tsx +23 -39
  93. package/src/react/FragmentRenderer.tsx +46 -0
  94. package/src/react/LoadableFieldReader.tsx +40 -0
  95. package/src/react/LoadableFieldRenderer.tsx +41 -0
  96. package/src/react/useImperativeReference.ts +9 -8
  97. package/src/react/useResult.ts +1 -0
  98. package/src/tests/__isograph/Economist/link/output_type.ts +2 -0
  99. package/src/tests/__isograph/Node/asEconomist/resolver_reader.ts +28 -0
  100. package/src/tests/__isograph/Node/link/output_type.ts +3 -0
  101. package/src/tests/__isograph/Query/linkedUpdate/entrypoint.ts +31 -0
  102. package/src/tests/__isograph/Query/linkedUpdate/normalization_ast.ts +95 -0
  103. package/src/tests/__isograph/Query/linkedUpdate/output_type.ts +3 -0
  104. package/src/tests/__isograph/Query/linkedUpdate/param_type.ts +51 -0
  105. package/src/tests/__isograph/Query/linkedUpdate/query_text.ts +20 -0
  106. package/src/tests/__isograph/Query/linkedUpdate/resolver_reader.ts +93 -0
  107. package/src/tests/__isograph/Query/meName/entrypoint.ts +4 -1
  108. package/src/tests/__isograph/Query/meName/query_text.ts +1 -1
  109. package/src/tests/__isograph/Query/meName/resolver_reader.ts +1 -0
  110. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +4 -1
  111. package/src/tests/__isograph/Query/meNameSuccessor/query_text.ts +1 -1
  112. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +3 -0
  113. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +4 -1
  114. package/src/tests/__isograph/Query/nodeField/query_text.ts +1 -1
  115. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +1 -0
  116. package/src/tests/__isograph/Query/startUpdate/entrypoint.ts +31 -0
  117. package/src/tests/__isograph/Query/startUpdate/normalization_ast.ts +51 -0
  118. package/src/tests/__isograph/Query/startUpdate/output_type.ts +3 -0
  119. package/src/tests/__isograph/Query/startUpdate/param_type.ts +26 -0
  120. package/src/tests/__isograph/Query/startUpdate/parameters_type.ts +3 -0
  121. package/src/tests/__isograph/Query/startUpdate/query_text.ts +11 -0
  122. package/src/tests/__isograph/Query/startUpdate/resolver_reader.ts +55 -0
  123. package/src/tests/__isograph/Query/subquery/entrypoint.ts +4 -1
  124. package/src/tests/__isograph/Query/subquery/query_text.ts +1 -1
  125. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +2 -0
  126. package/src/tests/__isograph/iso.ts +21 -1
  127. package/src/tests/__isograph/tsconfig.json +8 -0
  128. package/src/tests/normalizeData.test.ts +0 -1
  129. package/src/tests/startUpdate.test.ts +205 -0
  130. package/.turbo/turbo-compile-typescript.log +0 -4
@@ -12,15 +12,15 @@ function useImperativeReference(entrypoint) {
12
12
  return {
13
13
  fragmentReference: state !== react_disposable_state_1.UNASSIGNED_STATE ? state : null,
14
14
  loadFragmentReference: (variables, fetchOptions) => {
15
- const [networkRequest, disposeNetworkRequest] = (0, makeNetworkRequest_1.maybeMakeNetworkRequest)(environment, entrypoint, variables, fetchOptions);
15
+ const readerWithRefetchQueries = entrypoint.readerWithRefetchQueries.kind ===
16
+ 'ReaderWithRefetchQueriesLoader'
17
+ ? (0, PromiseWrapper_1.wrapPromise)(entrypoint.readerWithRefetchQueries.loader())
18
+ : (0, PromiseWrapper_1.wrapResolvedValue)(entrypoint.readerWithRefetchQueries);
19
+ const [networkRequest, disposeNetworkRequest] = (0, makeNetworkRequest_1.maybeMakeNetworkRequest)(environment, entrypoint, variables, readerWithRefetchQueries, fetchOptions !== null && fetchOptions !== void 0 ? fetchOptions : null);
16
20
  setState([
17
21
  {
18
22
  kind: 'FragmentReference',
19
- readerWithRefetchQueries: (0, PromiseWrapper_1.wrapResolvedValue)({
20
- kind: 'ReaderWithRefetchQueries',
21
- readerArtifact: entrypoint.readerWithRefetchQueries.readerArtifact,
22
- nestedRefetchQueries: entrypoint.readerWithRefetchQueries.nestedRefetchQueries,
23
- }),
23
+ readerWithRefetchQueries,
24
24
  root: { __link: IsographEnvironment_1.ROOT_ID, __typename: entrypoint.concreteType },
25
25
  variables,
26
26
  networkRequest,
@@ -1 +1 @@
1
- {"version":3,"file":"useResult.d.ts","sourceRoot":"","sources":["../../src/react/useResult.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EACjB,KAAK,qBAAqB,EAC3B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEL,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AAKtB,wBAAgB,SAAS,CACvB,cAAc,SAAS,qBAAqB,EAC5C,iBAAiB,EAEjB,iBAAiB,EAAE,iBAAiB,CAAC,cAAc,EAAE,iBAAiB,CAAC,EACvE,4BAA4B,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,GAAG,IAAI,GACzE,iBAAiB,CA8CnB;AAED,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EACzC,qBAAqB,EAAE,2BAA2B,QAWnD"}
1
+ {"version":3,"file":"useResult.d.ts","sourceRoot":"","sources":["../../src/react/useResult.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EACjB,KAAK,qBAAqB,EAC3B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEL,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AAKtB,wBAAgB,SAAS,CACvB,cAAc,SAAS,qBAAqB,EAC5C,iBAAiB,EAEjB,iBAAiB,EAAE,iBAAiB,CAAC,cAAc,EAAE,iBAAiB,CAAC,EACvE,4BAA4B,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,GAAG,IAAI,GACzE,iBAAiB,CA+CnB;AAED,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EACzC,qBAAqB,EAAE,2BAA2B,QAWnD"}
@@ -22,7 +22,7 @@ function useResult(fragmentReference, partialNetworkRequestOptions) {
22
22
  const data = (0, useReadAndSubscribe_1.useReadAndSubscribe)(fragmentReference, networkRequestOptions, readerWithRefetchQueries.readerArtifact.readerAst);
23
23
  const param = Object.assign({ data: data, parameters: fragmentReference.variables }, (readerWithRefetchQueries.readerArtifact.hasUpdatable
24
24
  ? {
25
- startUpdate: (0, startUpdate_1.getOrCreateCachedStartUpdate)(environment, fragmentReference, readerWithRefetchQueries.readerArtifact.fieldName),
25
+ startUpdate: (0, startUpdate_1.getOrCreateCachedStartUpdate)(environment, fragmentReference, readerWithRefetchQueries.readerArtifact.fieldName, networkRequestOptions),
26
26
  }
27
27
  : undefined));
28
28
  return readerWithRefetchQueries.readerArtifact.resolver(param);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isograph/react",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Use Isograph with React",
5
5
  "homepage": "https://isograph.dev",
6
6
  "main": "dist/index.js",
@@ -8,16 +8,15 @@
8
8
  "author": "Isograph Labs",
9
9
  "license": "MIT",
10
10
  "scripts": {
11
- "compile-typescript": "rm -rf dist/* && tsc -p tsconfig.pkg.json",
11
+ "compile-libs": "rimraf dist && tsc -p tsconfig.pkg.json",
12
12
  "compile-watch": "tsc -p tsconfig.pkg.json --watch",
13
13
  "test": "vitest run",
14
14
  "test-watch": "vitest watch",
15
15
  "coverage": "vitest run --coverage",
16
- "prepack": "pnpm run test && pnpm run compile-typescript",
16
+ "prepack": "pnpm run test && pnpm run compile-libs",
17
17
  "tsc": "tsc",
18
18
  "tsc-force": "tsc --build --clean && tsc --build --force",
19
- "iso": "cross-env ../../target/debug/isograph_cli --config ./isograph.config.json",
20
- "iso-watch": "cross-env ../../target/debug/isograph_cli --config ./isograph.config.json --watch"
19
+ "iso": "cross-env ISO_PRINT_ABSOLUTE_FILEPATH=1 ../../target/debug/isograph_cli --config ./isograph.config.json"
21
20
  },
22
21
  "dependencies": {
23
22
  "@isograph/disposable-types": "*",
@@ -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;
@@ -26,7 +31,7 @@ export type FragmentSubscription<TReadFromStore extends UnknownTReadFromStore> =
26
31
  newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
27
32
  ) => void;
28
33
  /** The value read out from the previous call to readButDoNotEvaluate */
29
- readonly encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>;
34
+ encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>;
30
35
  readonly fragmentReference: FragmentReference<TReadFromStore, any>;
31
36
  readonly readerAst: ReaderAst<TReadFromStore>;
32
37
  };
@@ -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 {
255
- return (t) => {
256
- try {
257
- return f(t);
258
- } catch {}
259
- };
262
+ function logAnyError(
263
+ environment: IsographEnvironment,
264
+ context: any,
265
+ f: () => void,
266
+ ) {
267
+ try {
268
+ f();
269
+ } catch (e) {
270
+ logMessage(environment, () => ({
271
+ kind: 'ErrorEncounteredInWithErrorHandling',
272
+ error: e,
273
+ context,
274
+ }));
275
+ }
260
276
  }
261
277
 
262
- function callSubscriptions(
278
+ export function callSubscriptions(
263
279
  environment: IsographEnvironment,
264
280
  recordsEncounteredWhenNormalizing: EncounteredIds,
265
281
  ) {
266
- environment.subscriptions.forEach(
267
- withErrorHandling((subscription) => {
282
+ environment.subscriptions.forEach((subscription) =>
283
+ logAnyError(environment, { situation: 'calling subscriptions' }, () => {
268
284
  switch (subscription.kind) {
269
285
  case 'FragmentSubscription': {
270
286
  // TODO if there are multiple components subscribed to the same
@@ -313,13 +329,25 @@ function callSubscriptions(
313
329
  }));
314
330
 
315
331
  if (mergedItem !== subscription.encounteredDataAndRecords.item) {
316
- subscription.callback(newEncounteredDataAndRecords);
332
+ logAnyError(
333
+ environment,
334
+ { situation: 'calling FragmentSubscription callback' },
335
+ () => {
336
+ subscription.callback(newEncounteredDataAndRecords);
337
+ },
338
+ );
339
+ subscription.encounteredDataAndRecords =
340
+ newEncounteredDataAndRecords;
317
341
  }
318
342
  }
319
343
  return;
320
344
  }
321
345
  case 'AnyRecords': {
322
- subscription.callback();
346
+ logAnyError(
347
+ environment,
348
+ { situation: 'calling AnyRecords callback' },
349
+ () => subscription.callback(),
350
+ );
323
351
  return;
324
352
  }
325
353
  case 'AnyChangesToRecord': {
@@ -328,7 +356,11 @@ function callSubscriptions(
328
356
  .get(subscription.recordLink.__typename)
329
357
  ?.has(subscription.recordLink.__link)
330
358
  ) {
331
- subscription.callback();
359
+ logAnyError(
360
+ environment,
361
+ { situation: 'calling AnyChangesToRecord callback' },
362
+ () => subscription.callback(),
363
+ );
332
364
  }
333
365
  return;
334
366
  }
@@ -379,9 +411,8 @@ function normalizeDataIntoRecord(
379
411
  normalizationAst: NormalizationAstNodes,
380
412
  networkResponseParentRecord: NetworkResponseObject,
381
413
  targetParentRecord: StoreRecord,
382
- targetParentRecordLink: Link,
414
+ targetParentRecordLink: StoreLink,
383
415
  variables: Variables,
384
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
385
416
  mutableEncounteredIds: EncounteredIds,
386
417
  ): RecordHasBeenUpdated {
387
418
  let recordHasBeenUpdated = false;
@@ -406,7 +437,6 @@ function normalizeDataIntoRecord(
406
437
  targetParentRecord,
407
438
  targetParentRecordLink,
408
439
  variables,
409
- nestedRefetchQueries,
410
440
  mutableEncounteredIds,
411
441
  );
412
442
  recordHasBeenUpdated =
@@ -421,7 +451,6 @@ function normalizeDataIntoRecord(
421
451
  targetParentRecord,
422
452
  targetParentRecordLink,
423
453
  variables,
424
- nestedRefetchQueries,
425
454
  mutableEncounteredIds,
426
455
  );
427
456
  recordHasBeenUpdated =
@@ -437,7 +466,7 @@ function normalizeDataIntoRecord(
437
466
  }
438
467
  }
439
468
  if (recordHasBeenUpdated) {
440
- let encounteredRecordsIds = insertIfNotExists(
469
+ let encounteredRecordsIds = insertEmptySetIfMissing(
441
470
  mutableEncounteredIds,
442
471
  targetParentRecordLink.__typename,
443
472
  );
@@ -447,7 +476,7 @@ function normalizeDataIntoRecord(
447
476
  return recordHasBeenUpdated;
448
477
  }
449
478
 
450
- export function insertIfNotExists<K, V>(map: Map<K, Set<V>>, key: K) {
479
+ export function insertEmptySetIfMissing<K, V>(map: Map<K, Set<V>>, key: K) {
451
480
  let result = map.get(key);
452
481
  if (result === undefined) {
453
482
  result = new Set();
@@ -487,9 +516,8 @@ function normalizeLinkedField(
487
516
  astNode: NormalizationLinkedField,
488
517
  networkResponseParentRecord: NetworkResponseObject,
489
518
  targetParentRecord: StoreRecord,
490
- targetParentRecordLink: Link,
519
+ targetParentRecordLink: StoreLink,
491
520
  variables: Variables,
492
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
493
521
  mutableEncounteredIds: EncounteredIds,
494
522
  ): RecordHasBeenUpdated {
495
523
  const networkResponseKey = getNetworkResponseKey(astNode);
@@ -513,7 +541,7 @@ function normalizeLinkedField(
513
541
 
514
542
  if (Array.isArray(networkResponseData)) {
515
543
  // TODO check astNode.plural or the like
516
- const dataIds: (Link | null)[] = [];
544
+ const dataIds: (StoreLink | null)[] = [];
517
545
  for (let i = 0; i < networkResponseData.length; i++) {
518
546
  const networkResponseObject = networkResponseData[i];
519
547
  if (networkResponseObject == null) {
@@ -527,7 +555,6 @@ function normalizeLinkedField(
527
555
  targetParentRecordLink,
528
556
  variables,
529
557
  i,
530
- nestedRefetchQueries,
531
558
  mutableEncounteredIds,
532
559
  );
533
560
 
@@ -554,7 +581,6 @@ function normalizeLinkedField(
554
581
  targetParentRecordLink,
555
582
  variables,
556
583
  null,
557
- nestedRefetchQueries,
558
584
  mutableEncounteredIds,
559
585
  );
560
586
 
@@ -586,9 +612,8 @@ function normalizeInlineFragment(
586
612
  astNode: NormalizationInlineFragment,
587
613
  networkResponseParentRecord: NetworkResponseObject,
588
614
  targetParentRecord: StoreRecord,
589
- targetParentRecordLink: Link,
615
+ targetParentRecordLink: StoreLink,
590
616
  variables: Variables,
591
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
592
617
  mutableEncounteredIds: EncounteredIds,
593
618
  ): RecordHasBeenUpdated {
594
619
  const typeToRefineTo = astNode.type;
@@ -600,7 +625,6 @@ function normalizeInlineFragment(
600
625
  targetParentRecord,
601
626
  targetParentRecordLink,
602
627
  variables,
603
- nestedRefetchQueries,
604
628
  mutableEncounteredIds,
605
629
  );
606
630
  return hasBeenModified;
@@ -610,7 +634,7 @@ function normalizeInlineFragment(
610
634
 
611
635
  function dataIdsAreTheSame(
612
636
  existingValue: DataTypeValue,
613
- newDataIds: (Link | null)[],
637
+ newDataIds: (StoreLink | null)[],
614
638
  ): boolean {
615
639
  if (Array.isArray(existingValue)) {
616
640
  if (newDataIds.length !== existingValue.length) {
@@ -635,10 +659,9 @@ function normalizeNetworkResponseObject(
635
659
  environment: IsographEnvironment,
636
660
  astNode: NormalizationLinkedField,
637
661
  networkResponseData: NetworkResponseObject,
638
- targetParentRecordLink: Link,
662
+ targetParentRecordLink: StoreLink,
639
663
  variables: Variables,
640
664
  index: number | null,
641
- nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[],
642
665
  mutableEncounteredIds: EncounteredIds,
643
666
  ): DataId /* The id of the modified or newly created item */ {
644
667
  const newStoreRecordId = getDataIdOfNetworkResponse(
@@ -668,7 +691,6 @@ function normalizeNetworkResponseObject(
668
691
  newStoreRecord,
669
692
  { __link: newStoreRecordId, __typename: __typename },
670
693
  variables,
671
- nestedRefetchQueries,
672
694
  mutableEncounteredIds,
673
695
  );
674
696
 
@@ -760,13 +782,14 @@ function getStoreKeyChunkForArgumentValue(
760
782
  }
761
783
 
762
784
  function getStoreKeyChunkForArgument(argument: Argument, variables: Variables) {
763
- let chunk = getStoreKeyChunkForArgumentValue(argument[1], variables);
785
+ const [argumentName, argumentValue] = argument;
786
+ let chunk = getStoreKeyChunkForArgumentValue(argumentValue, variables);
764
787
 
765
788
  if (typeof chunk === 'object') {
766
789
  chunk = JSON.stringify(stableCopy(chunk));
767
790
  }
768
791
 
769
- return `${FIRST_SPLIT_KEY}${argument[0]}${SECOND_SPLIT_KEY}${chunk}`;
792
+ return `${FIRST_SPLIT_KEY}${argumentName}${SECOND_SPLIT_KEY}${chunk}`;
770
793
  }
771
794
 
772
795
  function getNetworkResponseKey(
@@ -831,7 +854,7 @@ export const THIRD_SPLIT_KEY = '__';
831
854
 
832
855
  // Returns a key to look up an item in the store
833
856
  function getDataIdOfNetworkResponse(
834
- parentRecordLink: Link,
857
+ parentRecordLink: StoreLink,
835
858
  dataToNormalize: NetworkResponseObject,
836
859
  astNode: NormalizationLinkedField,
837
860
  variables: Variables,