@isograph/react 0.1.1 → 0.3.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 (160) hide show
  1. package/dist/core/FragmentReference.d.ts +25 -0
  2. package/dist/core/FragmentReference.d.ts.map +1 -0
  3. package/dist/core/FragmentReference.js +16 -0
  4. package/dist/core/IsographEnvironment.d.ts +89 -0
  5. package/dist/core/IsographEnvironment.d.ts.map +1 -0
  6. package/dist/core/IsographEnvironment.js +65 -0
  7. package/dist/core/PromiseWrapper.d.ts +28 -0
  8. package/dist/core/PromiseWrapper.d.ts.map +1 -0
  9. package/dist/core/PromiseWrapper.js +57 -0
  10. package/dist/core/areEqualWithDeepComparison.d.ts +5 -0
  11. package/dist/core/areEqualWithDeepComparison.d.ts.map +1 -0
  12. package/dist/core/areEqualWithDeepComparison.js +95 -0
  13. package/dist/core/cache.d.ts +44 -0
  14. package/dist/core/cache.d.ts.map +1 -0
  15. package/dist/core/cache.js +514 -0
  16. package/dist/core/check.d.ts +18 -0
  17. package/dist/core/check.d.ts.map +1 -0
  18. package/dist/core/check.js +127 -0
  19. package/dist/core/componentCache.d.ts +5 -0
  20. package/dist/core/componentCache.d.ts.map +1 -0
  21. package/dist/core/componentCache.js +38 -0
  22. package/dist/core/entrypoint.d.ts +69 -0
  23. package/dist/core/entrypoint.d.ts.map +1 -0
  24. package/dist/core/entrypoint.js +7 -0
  25. package/dist/core/garbageCollection.d.ts +13 -0
  26. package/dist/core/garbageCollection.d.ts.map +1 -0
  27. package/dist/core/garbageCollection.js +107 -0
  28. package/dist/core/logging.d.ts +69 -0
  29. package/dist/core/logging.d.ts.map +1 -0
  30. package/dist/core/logging.js +19 -0
  31. package/dist/core/makeNetworkRequest.d.ts +9 -0
  32. package/dist/core/makeNetworkRequest.d.ts.map +1 -0
  33. package/dist/core/makeNetworkRequest.js +118 -0
  34. package/dist/core/read.d.ts +27 -0
  35. package/dist/core/read.d.ts.map +1 -0
  36. package/dist/core/read.js +478 -0
  37. package/dist/core/reader.d.ts +87 -0
  38. package/dist/core/reader.d.ts.map +1 -0
  39. package/dist/core/reader.js +2 -0
  40. package/dist/core/util.d.ts +19 -0
  41. package/dist/core/util.d.ts.map +1 -0
  42. package/dist/core/util.js +2 -0
  43. package/dist/index.d.ts +26 -120
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +57 -306
  46. package/dist/loadable-hooks/useClientSideDefer.d.ts +16 -0
  47. package/dist/loadable-hooks/useClientSideDefer.d.ts.map +1 -0
  48. package/dist/loadable-hooks/useClientSideDefer.js +13 -0
  49. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts +34 -0
  50. package/dist/loadable-hooks/useConnectionSpecPagination.d.ts.map +1 -0
  51. package/dist/loadable-hooks/useConnectionSpecPagination.js +160 -0
  52. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +6 -0
  53. package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts.map +1 -0
  54. package/dist/loadable-hooks/useImperativeExposedMutationField.js +14 -0
  55. package/dist/loadable-hooks/useImperativeLoadableField.d.ts +17 -0
  56. package/dist/loadable-hooks/useImperativeLoadableField.d.ts.map +1 -0
  57. package/dist/loadable-hooks/useImperativeLoadableField.js +14 -0
  58. package/dist/loadable-hooks/useSkipLimitPagination.d.ts +27 -0
  59. package/dist/loadable-hooks/useSkipLimitPagination.d.ts.map +1 -0
  60. package/dist/loadable-hooks/useSkipLimitPagination.js +162 -0
  61. package/dist/react/FragmentReader.d.ts +16 -0
  62. package/dist/react/FragmentReader.d.ts.map +1 -0
  63. package/dist/{EntrypointReader.js → react/FragmentReader.js} +7 -5
  64. package/dist/react/IsographEnvironmentProvider.d.ts +11 -0
  65. package/dist/react/IsographEnvironmentProvider.d.ts.map +1 -0
  66. package/dist/{IsographEnvironment.js → react/IsographEnvironmentProvider.js} +4 -22
  67. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts +10 -0
  68. package/dist/react/RenderAfterCommit__DO_NOT_USE.d.ts.map +1 -0
  69. package/dist/react/RenderAfterCommit__DO_NOT_USE.js +15 -0
  70. package/dist/react/useImperativeReference.d.ts +12 -0
  71. package/dist/react/useImperativeReference.d.ts.map +1 -0
  72. package/dist/react/useImperativeReference.js +35 -0
  73. package/dist/react/useLazyReference.d.ts +10 -0
  74. package/dist/react/useLazyReference.d.ts.map +1 -0
  75. package/dist/react/useLazyReference.js +21 -0
  76. package/dist/react/useReadAndSubscribe.d.ts +20 -0
  77. package/dist/react/useReadAndSubscribe.d.ts.map +1 -0
  78. package/dist/react/useReadAndSubscribe.js +40 -0
  79. package/dist/react/useRerenderOnChange.d.ts +8 -0
  80. package/dist/react/useRerenderOnChange.d.ts.map +1 -0
  81. package/dist/react/useRerenderOnChange.js +22 -0
  82. package/dist/react/useResult.d.ts +9 -0
  83. package/dist/react/useResult.d.ts.map +1 -0
  84. package/dist/react/useResult.js +39 -0
  85. package/docs/how-useLazyReference-works.md +117 -0
  86. package/isograph.config.json +7 -0
  87. package/package.json +18 -9
  88. package/schema.graphql +17 -0
  89. package/src/core/FragmentReference.ts +49 -0
  90. package/src/core/IsographEnvironment.ts +192 -0
  91. package/src/core/PromiseWrapper.ts +86 -0
  92. package/src/core/areEqualWithDeepComparison.ts +112 -0
  93. package/src/core/cache.ts +835 -0
  94. package/src/core/check.ts +207 -0
  95. package/src/core/componentCache.ts +62 -0
  96. package/src/core/entrypoint.ts +106 -0
  97. package/src/core/garbageCollection.ts +173 -0
  98. package/src/core/logging.ts +116 -0
  99. package/src/core/makeNetworkRequest.ts +175 -0
  100. package/src/core/read.ts +722 -0
  101. package/src/core/reader.ts +160 -0
  102. package/src/core/util.ts +27 -0
  103. package/src/index.ts +99 -0
  104. package/src/loadable-hooks/useClientSideDefer.ts +58 -0
  105. package/src/loadable-hooks/useConnectionSpecPagination.ts +331 -0
  106. package/src/loadable-hooks/useImperativeExposedMutationField.ts +17 -0
  107. package/src/loadable-hooks/useImperativeLoadableField.ts +52 -0
  108. package/src/loadable-hooks/useSkipLimitPagination.ts +352 -0
  109. package/src/react/FragmentReader.tsx +43 -0
  110. package/src/react/IsographEnvironmentProvider.tsx +33 -0
  111. package/src/react/RenderAfterCommit__DO_NOT_USE.tsx +17 -0
  112. package/src/react/useImperativeReference.ts +68 -0
  113. package/src/react/useLazyReference.ts +42 -0
  114. package/src/react/useReadAndSubscribe.ts +81 -0
  115. package/src/react/useRerenderOnChange.ts +38 -0
  116. package/src/react/useResult.ts +73 -0
  117. package/src/tests/__isograph/Query/meName/entrypoint.ts +52 -0
  118. package/src/tests/__isograph/Query/meName/output_type.ts +3 -0
  119. package/src/tests/__isograph/Query/meName/param_type.ts +9 -0
  120. package/src/tests/__isograph/Query/meName/resolver_reader.ts +33 -0
  121. package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +90 -0
  122. package/src/tests/__isograph/Query/meNameSuccessor/output_type.ts +3 -0
  123. package/src/tests/__isograph/Query/meNameSuccessor/param_type.ts +14 -0
  124. package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +57 -0
  125. package/src/tests/__isograph/Query/nodeField/entrypoint.ts +57 -0
  126. package/src/tests/__isograph/Query/nodeField/output_type.ts +3 -0
  127. package/src/tests/__isograph/Query/nodeField/param_type.ts +10 -0
  128. package/src/tests/__isograph/Query/nodeField/parameters_type.ts +3 -0
  129. package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +38 -0
  130. package/src/tests/__isograph/Query/subquery/entrypoint.ts +67 -0
  131. package/src/tests/__isograph/Query/subquery/output_type.ts +3 -0
  132. package/src/tests/__isograph/Query/subquery/param_type.ts +12 -0
  133. package/src/tests/__isograph/Query/subquery/parameters_type.ts +3 -0
  134. package/src/tests/__isograph/Query/subquery/resolver_reader.ts +47 -0
  135. package/src/tests/__isograph/iso.ts +99 -0
  136. package/src/tests/garbageCollection.test.ts +142 -0
  137. package/src/tests/meNameSuccessor.ts +25 -0
  138. package/src/tests/nodeQuery.ts +19 -0
  139. package/src/tests/normalizeData.test.ts +120 -0
  140. package/src/tests/tsconfig.json +21 -0
  141. package/tsconfig.json +6 -0
  142. package/tsconfig.pkg.json +7 -1
  143. package/vitest.config.ts +20 -0
  144. package/dist/EntrypointReader.d.ts +0 -6
  145. package/dist/IsographEnvironment.d.ts +0 -56
  146. package/dist/PromiseWrapper.d.ts +0 -13
  147. package/dist/PromiseWrapper.js +0 -22
  148. package/dist/cache.d.ts +0 -26
  149. package/dist/cache.js +0 -274
  150. package/dist/componentCache.d.ts +0 -6
  151. package/dist/componentCache.js +0 -31
  152. package/dist/useImperativeReference.d.ts +0 -8
  153. package/dist/useImperativeReference.js +0 -28
  154. package/src/EntrypointReader.tsx +0 -20
  155. package/src/IsographEnvironment.tsx +0 -120
  156. package/src/PromiseWrapper.ts +0 -29
  157. package/src/cache.tsx +0 -484
  158. package/src/componentCache.ts +0 -41
  159. package/src/index.tsx +0 -617
  160. package/src/useImperativeReference.ts +0 -58
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useLazyReference = useLazyReference;
4
+ const IsographEnvironmentProvider_1 = require("./IsographEnvironmentProvider");
5
+ const cache_1 = require("../core/cache");
6
+ const react_disposable_state_1 = require("@isograph/react-disposable-state");
7
+ const logging_1 = require("../core/logging");
8
+ function useLazyReference(entrypoint, variables, fetchOptions) {
9
+ const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
10
+ if ((entrypoint === null || entrypoint === void 0 ? void 0 : entrypoint.kind) !== 'Entrypoint') {
11
+ // TODO have a separate error logger
12
+ (0, logging_1.logMessage)(environment, {
13
+ kind: 'NonEntrypointReceived',
14
+ entrypoint,
15
+ });
16
+ }
17
+ const cache = (0, cache_1.getOrCreateCacheForArtifact)(environment, entrypoint, variables, fetchOptions);
18
+ return {
19
+ fragmentReference: (0, react_disposable_state_1.useLazyDisposableState)(cache).state,
20
+ };
21
+ }
@@ -0,0 +1,20 @@
1
+ import { FragmentReference, ExtractData } from '../core/FragmentReference';
2
+ import { NetworkRequestReaderOptions, WithEncounteredRecords } from '../core/read';
3
+ import type { ReaderAst } from '../core/reader';
4
+ /**
5
+ * Read the data from a fragment reference and subscribe to updates.
6
+ */
7
+ export declare function useReadAndSubscribe<TReadFromStore extends {
8
+ parameters: object;
9
+ data: object;
10
+ }>(fragmentReference: FragmentReference<TReadFromStore, any>, networkRequestOptions: NetworkRequestReaderOptions, readerAst: ReaderAst<TReadFromStore>): ExtractData<TReadFromStore>;
11
+ export declare function useSubscribeToMultiple<TReadFromStore extends {
12
+ parameters: object;
13
+ data: object;
14
+ }>(items: ReadonlyArray<{
15
+ records: WithEncounteredRecords<TReadFromStore>;
16
+ callback: (updatedRecords: WithEncounteredRecords<TReadFromStore>) => void;
17
+ fragmentReference: FragmentReference<TReadFromStore, any>;
18
+ readerAst: ReaderAst<TReadFromStore>;
19
+ }>): void;
20
+ //# sourceMappingURL=useReadAndSubscribe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useReadAndSubscribe.d.ts","sourceRoot":"","sources":["../../src/react/useReadAndSubscribe.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EAEjB,WAAW,EACZ,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAE3B,sBAAsB,EACvB,MAAM,cAAc,CAAC;AAItB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,SAAS;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAE3D,iBAAiB,EAAE,iBAAiB,CAAC,cAAc,EAAE,GAAG,CAAC,EACzD,qBAAqB,EAAE,2BAA2B,EAClD,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,GACnC,WAAW,CAAC,cAAc,CAAC,CAY7B;AAED,wBAAgB,sBAAsB,CACpC,cAAc,SAAS;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAE3D,KAAK,EAAE,aAAa,CAAC;IACnB,OAAO,EAAE,sBAAsB,CAAC,cAAc,CAAC,CAAC;IAChD,QAAQ,EAAE,CAAC,cAAc,EAAE,sBAAsB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAC3E,iBAAiB,EAAE,iBAAiB,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IAC1D,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;CACtC,CAAC,QAiCH"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useReadAndSubscribe = useReadAndSubscribe;
4
+ exports.useSubscribeToMultiple = useSubscribeToMultiple;
5
+ const react_1 = require("react");
6
+ const FragmentReference_1 = require("../core/FragmentReference");
7
+ const read_1 = require("../core/read");
8
+ const useRerenderOnChange_1 = require("./useRerenderOnChange");
9
+ const IsographEnvironmentProvider_1 = require("./IsographEnvironmentProvider");
10
+ const cache_1 = require("../core/cache");
11
+ /**
12
+ * Read the data from a fragment reference and subscribe to updates.
13
+ */
14
+ function useReadAndSubscribe(fragmentReference, networkRequestOptions, readerAst) {
15
+ const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
16
+ const [readOutDataAndRecords, setReadOutDataAndRecords] = (0, react_1.useState)(() => (0, read_1.readButDoNotEvaluate)(environment, fragmentReference, networkRequestOptions));
17
+ (0, useRerenderOnChange_1.useRerenderOnChange)(readOutDataAndRecords, fragmentReference, setReadOutDataAndRecords, readerAst);
18
+ return readOutDataAndRecords.item;
19
+ }
20
+ function useSubscribeToMultiple(items) {
21
+ const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
22
+ (0, react_1.useEffect)(() => {
23
+ const cleanupFns = items.map(({ records, callback, fragmentReference, readerAst }) => {
24
+ return (0, cache_1.subscribe)(environment, records, fragmentReference, callback, readerAst);
25
+ });
26
+ return () => {
27
+ cleanupFns.forEach((loader) => {
28
+ loader();
29
+ });
30
+ };
31
+ },
32
+ // By analogy to useReadAndSubscribe, we can have an empty dependency array?
33
+ // Maybe callback has to be depended on. I don't know!
34
+ // TODO find out
35
+ [
36
+ items
37
+ .map(({ fragmentReference }) => (0, FragmentReference_1.stableIdForFragmentReference)(fragmentReference))
38
+ .join('.'),
39
+ ]);
40
+ }
@@ -0,0 +1,8 @@
1
+ import { WithEncounteredRecords } from '../core/read';
2
+ import { FragmentReference } from '../core/FragmentReference';
3
+ import type { ReaderAst } from '../core/reader';
4
+ export declare function useRerenderOnChange<TReadFromStore extends {
5
+ parameters: object;
6
+ data: object;
7
+ }>(encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>, fragmentReference: FragmentReference<any, any>, setEncounteredDataAndRecords: (data: WithEncounteredRecords<TReadFromStore>) => void, readerAst: ReaderAst<TReadFromStore>): void;
8
+ //# sourceMappingURL=useRerenderOnChange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRerenderOnChange.d.ts","sourceRoot":"","sources":["../../src/react/useRerenderOnChange.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAIhD,wBAAgB,mBAAmB,CACjC,cAAc,SAAS;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAE3D,yBAAyB,EAAE,sBAAsB,CAAC,cAAc,CAAC,EACjE,iBAAiB,EAAE,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,EAC9C,4BAA4B,EAAE,CAC5B,IAAI,EAAE,sBAAsB,CAAC,cAAc,CAAC,KACzC,IAAI,EACT,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,QAoBrC"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useRerenderOnChange = useRerenderOnChange;
4
+ const react_1 = require("react");
5
+ const cache_1 = require("../core/cache");
6
+ const IsographEnvironmentProvider_1 = require("./IsographEnvironmentProvider");
7
+ // TODO add unit tests for this. Add integration tests that test
8
+ // behavior when the encounteredRecords underneath a fragment change.
9
+ function useRerenderOnChange(encounteredDataAndRecords, fragmentReference, setEncounteredDataAndRecords, readerAst) {
10
+ const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
11
+ (0, react_1.useEffect)(() => {
12
+ return (0, cache_1.subscribe)(environment, encounteredDataAndRecords, fragmentReference, (newEncounteredDataAndRecords) => {
13
+ setEncounteredDataAndRecords(newEncounteredDataAndRecords);
14
+ }, readerAst);
15
+ // Note: this is an empty array on purpose:
16
+ // - the fragment reference is stable for the life of the component
17
+ // - ownership of encounteredDataAndRecords is transferred into the
18
+ // environment
19
+ // - though maybe we need to include setEncounteredDataAndRecords in
20
+ // the dependency array
21
+ }, []);
22
+ }
@@ -0,0 +1,9 @@
1
+ import { FragmentReference } from '../core/FragmentReference';
2
+ import { NetworkRequestReaderOptions } from '../core/read';
3
+ import { PromiseWrapper } from '../core/PromiseWrapper';
4
+ export declare function useResult<TReadFromStore extends {
5
+ parameters: object;
6
+ data: object;
7
+ }, TClientFieldValue>(fragmentReference: FragmentReference<TReadFromStore, TClientFieldValue>, partialNetworkRequestOptions?: Partial<NetworkRequestReaderOptions> | void): TClientFieldValue;
8
+ export declare function maybeUnwrapNetworkRequest(networkRequest: PromiseWrapper<void, any>, networkRequestOptions: NetworkRequestReaderOptions): void;
9
+ //# sourceMappingURL=useResult.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResult.d.ts","sourceRoot":"","sources":["../../src/react/useResult.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAEL,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAEL,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAEhC,wBAAgB,SAAS,CACvB,cAAc,SAAS;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC3D,iBAAiB,EAEjB,iBAAiB,EAAE,iBAAiB,CAAC,cAAc,EAAE,iBAAiB,CAAC,EACvE,4BAA4B,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,GAAG,IAAI,GACzE,iBAAiB,CAqCnB;AAED,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EACzC,qBAAqB,EAAE,2BAA2B,QAWnD"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useResult = useResult;
4
+ exports.maybeUnwrapNetworkRequest = maybeUnwrapNetworkRequest;
5
+ const IsographEnvironmentProvider_1 = require("../react/IsographEnvironmentProvider");
6
+ const componentCache_1 = require("../core/componentCache");
7
+ const useReadAndSubscribe_1 = require("./useReadAndSubscribe");
8
+ const read_1 = require("../core/read");
9
+ const PromiseWrapper_1 = require("../core/PromiseWrapper");
10
+ function useResult(fragmentReference, partialNetworkRequestOptions) {
11
+ const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
12
+ const networkRequestOptions = (0, read_1.getNetworkRequestOptionsWithDefaults)(partialNetworkRequestOptions);
13
+ maybeUnwrapNetworkRequest(fragmentReference.networkRequest, networkRequestOptions);
14
+ const readerWithRefetchQueries = (0, PromiseWrapper_1.readPromise)(fragmentReference.readerWithRefetchQueries);
15
+ switch (readerWithRefetchQueries.readerArtifact.kind) {
16
+ case 'ComponentReaderArtifact': {
17
+ // @ts-expect-error
18
+ return (0, componentCache_1.getOrCreateCachedComponent)(environment, readerWithRefetchQueries.readerArtifact.componentName, fragmentReference, networkRequestOptions);
19
+ }
20
+ case 'EagerReaderArtifact': {
21
+ const data = (0, useReadAndSubscribe_1.useReadAndSubscribe)(fragmentReference, networkRequestOptions, readerWithRefetchQueries.readerArtifact.readerAst);
22
+ const param = {
23
+ data: data,
24
+ parameters: fragmentReference.variables,
25
+ };
26
+ return readerWithRefetchQueries.readerArtifact.resolver(param);
27
+ }
28
+ }
29
+ }
30
+ function maybeUnwrapNetworkRequest(networkRequest, networkRequestOptions) {
31
+ const state = (0, PromiseWrapper_1.getPromiseState)(networkRequest);
32
+ if (state.kind === 'Err' && networkRequestOptions.throwOnNetworkError) {
33
+ throw state.error;
34
+ }
35
+ else if (state.kind === 'Pending' &&
36
+ networkRequestOptions.suspendIfInFlight) {
37
+ throw state.promise;
38
+ }
39
+ }
@@ -0,0 +1,117 @@
1
+ # How does `useLazyReference` work, at an extremely detailed level?
2
+
3
+ ## High level explanation
4
+
5
+ [`useLazyReference`](https://github.com/isographlabs/isograph/blob/main/libs/isograph-react/src/useLazyReference.ts) is the developer-facing API. From the user's perspective, they call this to make a network request during the initial render, and then receive a query reference that they can use to read out the data from that network request.
6
+
7
+ Our goal is to only create a single network request, even if `useLazyReference` is called multiple times. We cannot always achieve this (it is impossible in React's programming model), but we can get pretty close.
8
+
9
+ ## Definitions
10
+
11
+ First, some definitions. Don't worry about these, but refer back to them if necessary:
12
+
13
+ - A `ParentCache` is initially empty, but can store a single `CacheItem`. The `ParentCache` must be stable across renders.
14
+ - A `CacheItem` is an object storing some value and is in one of three states: `InParentCacheAndNotDisposed`, `NotInParentCacheAndNotDisposed`, and `NotInParentCacheAndDisposed`.
15
+ - An invariant is that these APIs will never return a disposed `CacheItem` to the user.
16
+ - Creating the `CacheItem` is allowed to have side effects (e.g. making network requests in this example). We want to create as few `CacheItem`'s as possible.
17
+ - A `CacheItem` can be disposed, at which point it can perform some cleanup. Another guarantee is that every `CacheItem` we create will be disposed (i.e. no memory leaks).
18
+ - A `CacheItem` can also have (undisposed) temporary retains and permanent retains. If it has neither, it gets disposed.
19
+ - When a `CacheItem` has no more undisposed temporary retains, it removes itself from the parent cache.
20
+ - When a `CacheItem` has no more undisposed temporary retains **and** no more undisposed permanent retains, it disposes itself.
21
+ - A **temporary retain** is a retain that last 5 seconds or until you explicitly dispose it.
22
+ - A **permanent retain** is a retain that lasts until you explicitly dispose it.
23
+ - If a `CacheItem` is in a `ParentCache`, it must have at least one temporary retain. (Permanent retains are irrelevant here!)
24
+
25
+ ## How it all works
26
+
27
+ Okay, so now the actual description of how this all works:
28
+
29
+ - When `useLazyReference` is called, we get-or-create a `ParentCache`.
30
+ - When the cache is filled with a `CacheItem`, we make a network request and return a query reference.
31
+ - `useLazyReference` calls `useLazyDisposableState`, which calls `useCachedPrecommitValue` with that `ParentCache` and a callback (1).
32
+ - When `useCachedPrecommitValue` is called and has not committed, it will:
33
+ - If that `ParentCache` is empty, fill the cache with a `CacheItem` and create a "temporary retain" (2), and return that query reference to to `useLazyDisposableState`. (3a)
34
+ - If that `ParentCache` is not empty, we create a "temporary retain" (2) on that `CacheItem`, and return the query reference already in the cache to `useLazyDisposableState`. (3b)
35
+ - When `useCachedPrecommitValue` commits, it will:
36
+ - Check whether the `CacheItem` that we received during the render is disposed. If not disposed, we clear the temporary retain (2), permanently retain the item, and pass that value to the callback (1).
37
+ - If the item we received during the render is disposed, check whether the `ParentCache` is filled. (Another render might have filled the cache!) If so, permanently retain that `CacheItem` and pass the value to that callback (1).
38
+ - If that `ParentCache` is not filled, create a `CacheItem` **but not put it in the `ParentCache`**, permanently retain it, and pass it to the callback. (1)
39
+ - When `useCachedPrecommitValue` is called after it has committed, it will return `null`.
40
+ - So, at this point, we either have returned a value to `useLazyDisposableState` (3a and 3b), or we have committed and some callback has executed.
41
+ - The callback (1) that `useLazyDisposableState` passed to `useCachedPrecommitValue` stores the permanently retained `CacheItem` in a ref.
42
+ - `useLazyDisposableState` then returns either the value returned from `useCachedPrecommitValue` or the one in the ref.
43
+ - If we have some logic error and neither of these contain values, we throw an error.
44
+ - When the `useLazyDisposableState` unmounts (i.e. which only occurs if it has committed), we dispose of the permanent retain.
45
+
46
+ ## What about these temporary and permanent retains?
47
+
48
+ Let's go through some scenarios, in order to show how these temporary and permanent retains prevent us from making redundant network requests, and how they allow us to avoid memory leaks.
49
+
50
+ ### Happiest path with no suspense
51
+
52
+ In the happiest path, `useLazyReference` is called once and commits in less than 5 seconds.
53
+
54
+ - `useLazyReference` is called. The `ParentCache` becomes filled, triggering a network request. The `CacheItem` has one temporary retain.
55
+ - `useLazyReference` commits. The temporary retain is cleared, causing the `ParentCache` to become empty. The `CacheItem` is permanently retained.
56
+ - `useLazyReference` unmounts. The `CacheItem` is disposed.
57
+
58
+ Subsequent calls to `useLazyReference` from unmounted components will see an empty cache and make new network requests.
59
+
60
+ ### Happy path with suspense
61
+
62
+ In the happy path, we call `useLazyReference`, suspend, then call it again and commit.
63
+
64
+ - `useLazyReference` is called (render 1). The `ParentCache` becomes filled, triggering a network request. The `CacheItem` has one temporary retain.
65
+ - `useLazyReference` is called again (render 2). The `ParentCache` is already filled, so we simply create another temporary retain.
66
+ - `useLazyReference` commits (from render 2). The temporary retain is cleared, but there is still a temporary retain outstanding, so we do not empty the `ParentCache`. The `CacheItem` is permanently retained.
67
+ - After five seconds, the temporary retain clears itself, causing the `ParentCache` to become empty.
68
+ - `useLazyReference` unmounts. The `CacheItem` is disposed.
69
+
70
+ A key thing to note here is that, in the presence of suspense, we are never informed that render 1 will never commit! Hence, the temporary retain is important.
71
+
72
+ ### Multiple components making the same network request
73
+
74
+ Another thing to note is that we cannot distinguish between the same component rendering multiple times (due to suspense) from two identical components rendering. That is, the user must help us distinguish these two possibilities; library code cannot do it. So, let's consider two components rendering and mounting.
75
+
76
+ - `useLazyReference` is called (render 1). The `ParentCache` becomes filled, triggering a network request. The `CacheItem` has one temporary retain.
77
+ - `useLazyReference` is called again (render 2). The `ParentCache` is already filled, so we simply create another temporary retain.
78
+ - `useLazyReference` commits (from render 1 or 2). The temporary retain is cleared, but there is still a temporary retain outstanding, so we do not empty the `ParentCache`. The `CacheItem` is permanently retained.
79
+ - `useLazyReference` commits (from the other render). The last temporary retain is cleared, so we empty the `ParentCache`. The `CacheItem` is permanently retained.
80
+ - When both of these components unmount, the `CacheItem` is finally disposed.
81
+
82
+ ### A component that renders multiple times, but never mounts
83
+
84
+ Another scenario we must handle is a component rendering multiple times without mounting. For example, a component might render, something suspends, then a parent component unmounts. So, our component never commits. In scenarios like this, we do not want memory leaks!
85
+
86
+ - `useLazyReference` is called initially. The `ParentCache` becomes filled, triggering a network request. The `CacheItem` has one temporary retain.
87
+ - `useLazyReference` is potentially called again (potentially multiple times). The `ParentCache` is already filled, so we simply create another temporary retain for each render.
88
+ - Some parent component unmounts, so none of these renders will ever commit.
89
+ - 5 seconds after the last render, we clean up the last temporary retain and remove the item from the `ParentCache`.
90
+
91
+ ### A component that takes too long to commit
92
+
93
+ A scenario we must face is a component that takes too long to commit (e.g. >5 seconds.) I don't know whether 5 seconds is the optimal amount of time to wait, maybe 30 seconds is more reasonable!
94
+
95
+ - `useLazyReference` is called initially. The `ParentCache` becomes filled, triggering a network request. The `CacheItem` has one temporary retain.
96
+ - For whatever reason, React is slow. After 5 seconds, the temporary retain is cleared, and the `ParentCache` is cleared.
97
+ - The render commits. We find that the item we created is disposed **and** the `ParentCache` is empty. So, we create a new `CacheItem` and permanently retain it.
98
+
99
+ ## Conclusion: why?
100
+
101
+ The reason we go through this much effort is because we must deal with several facts:
102
+
103
+ - We have no guarantee that a render will be followed by a commit.
104
+ - If a render is not followed by a commit, we will never be informed.
105
+ - We cannot distinguish between a render that will never commit from a render that will commit in the future.
106
+
107
+ In light of these, the best we can do is to create temporary retains during render, and permanent retains only when a component commits.
108
+
109
+ ### Why clean stuff up?
110
+
111
+ There are two reasons that we dispose of `CacheItem`s.
112
+
113
+ First is that we do not want the memory usage of the app to grow without bound as the user uses our app.
114
+
115
+ Second, consider a `CacheItem` that makes a network request during initial render of a page. If the user loaded the data for that page, then navigated away, then navigated back after a long time, then we would want to make a new network request for that data. If we do not clean up after ourselves, we would be locked into forever re-using the existing network response.
116
+
117
+ Note that avoiding that network request (which is reasonable if e.g. the user navigates back after 10 seconds) can be done by, when the `CacheItem` is created, choosing to re-use cached data and not make a network request.
@@ -0,0 +1,7 @@
1
+ {
2
+ "project_root": "./src/tests",
3
+ "schema": "./schema.graphql",
4
+ "options": {
5
+ "on_invalid_id_type": "error"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isograph/react",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Use Isograph with React",
5
5
  "homepage": "https://isograph.dev",
6
6
  "main": "dist/index.js",
@@ -10,25 +10,34 @@
10
10
  "scripts": {
11
11
  "compile": "rm -rf dist/* && tsc -p tsconfig.pkg.json",
12
12
  "compile-watch": "tsc -p tsconfig.pkg.json --watch",
13
- "test": "echo no tests yet",
13
+ "test": "vitest run",
14
14
  "test-watch": "vitest watch",
15
15
  "coverage": "vitest run --coverage",
16
- "prepack": "yarn run test && yarn run compile"
16
+ "prepack": "pnpm run test && pnpm run compile",
17
+ "tsc": "tsc",
18
+ "iso": "../../target/debug/isograph_cli --config ./isograph.config.json",
19
+ "iso-watch": "../../target/debug/isograph_cli --config ./isograph.config.json --watch"
17
20
  },
18
21
  "dependencies": {
19
- "@isograph/disposable-types": "0.1.1",
22
+ "@isograph/disposable-types": "*",
20
23
  "@isograph/react-disposable-state": "*",
21
- "react": "^18.2.0"
24
+ "@isograph/reference-counted-pointer": "*"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "18.3.1"
22
28
  },
23
29
  "devDependencies": {
24
- "@types/react": "^18.0.31",
30
+ "@babel/preset-typescript": "^7.24.7",
31
+ "@types/react": "18.3.1",
25
32
  "react-test-renderer": "^18.2.0",
26
- "vitest": "^0.29.8",
27
- "typescript": "^5.0.3"
33
+ "typescript": "5.6.3",
34
+ "vite-plugin-babel": "^1.2.0",
35
+ "vite-plugin-commonjs": "^0.10.1"
28
36
  },
29
37
  "repository": {
30
38
  "type": "git",
31
39
  "url": "git+https://github.com/isographlabs/isograph.git",
32
40
  "directory": "libs/isograph-react"
33
- }
41
+ },
42
+ "sideEffects": false
34
43
  }
package/schema.graphql ADDED
@@ -0,0 +1,17 @@
1
+ type Query {
2
+ me: Economist!
3
+ you: Economist!
4
+ node(id: ID!): Node
5
+ query: Query!
6
+ }
7
+
8
+ type Economist implements Node {
9
+ id: ID!
10
+ name: String!
11
+ predecessor: Economist
12
+ successor: Economist
13
+ }
14
+
15
+ interface Node {
16
+ id: ID!
17
+ }
@@ -0,0 +1,49 @@
1
+ import { type Link } from './IsographEnvironment';
2
+ import { ReaderWithRefetchQueries } from '../core/entrypoint';
3
+ import { PromiseWrapper } from './PromiseWrapper';
4
+
5
+ // TODO type this better
6
+ export type VariableValue = string | number | boolean | null | object;
7
+
8
+ export type Variables = { readonly [index: string]: VariableValue };
9
+
10
+ export type ExtractData<T> = T extends {
11
+ data: infer D extends object;
12
+ }
13
+ ? D
14
+ : never;
15
+
16
+ export type ExtractParameters<T> = T extends {
17
+ parameters: infer P extends Variables;
18
+ }
19
+ ? P
20
+ : Variables;
21
+
22
+ export type FragmentReference<
23
+ TReadFromStore extends { parameters: object; data: object },
24
+ TClientFieldValue,
25
+ > = {
26
+ readonly kind: 'FragmentReference';
27
+ readonly readerWithRefetchQueries: PromiseWrapper<
28
+ ReaderWithRefetchQueries<TReadFromStore, TClientFieldValue>
29
+ >;
30
+ readonly root: Link;
31
+ readonly variables: ExtractParameters<TReadFromStore>;
32
+ readonly networkRequest: PromiseWrapper<void, any>;
33
+ };
34
+
35
+ export function stableIdForFragmentReference(
36
+ fragmentReference: FragmentReference<any, any>,
37
+ ): string {
38
+ return `${fragmentReference.root.__typename}/${fragmentReference.root.__link}/TODO_FRAGMENT_NAME/${serializeVariables(fragmentReference.variables ?? {})}`;
39
+ }
40
+
41
+ function serializeVariables(variables: Variables) {
42
+ let s = '';
43
+ const keys = Object.keys(variables);
44
+ keys.sort();
45
+ for (const key of keys) {
46
+ s += `${key}:${variables[key]},`;
47
+ }
48
+ return s;
49
+ }
@@ -0,0 +1,192 @@
1
+ import { ParentCache } from '@isograph/react-disposable-state';
2
+ import { RetainedQuery } from './garbageCollection';
3
+ import { WithEncounteredRecords } from './read';
4
+ import { FragmentReference, Variables } from './FragmentReference';
5
+ import { PromiseWrapper, wrapPromise } from './PromiseWrapper';
6
+ import { IsographEntrypoint } from './entrypoint';
7
+ import type { ReaderAst } from './reader';
8
+ import { LogFunction, WrappedLogFunction } from './logging';
9
+
10
+ export type ComponentOrFieldName = string;
11
+ export type StringifiedArgs = string;
12
+ type ComponentCache = {
13
+ [key: DataId]: {
14
+ [key: ComponentOrFieldName]: { [key: StringifiedArgs]: React.FC<any> };
15
+ };
16
+ };
17
+
18
+ export type FragmentSubscription<
19
+ TReadFromStore extends { parameters: object; data: object },
20
+ > = {
21
+ readonly kind: 'FragmentSubscription';
22
+ readonly callback: (
23
+ newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
24
+ ) => void;
25
+ /** The value read out from the previous call to readButDoNotEvaluate */
26
+ readonly encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>;
27
+ readonly fragmentReference: FragmentReference<TReadFromStore, any>;
28
+ readonly readerAst: ReaderAst<TReadFromStore>;
29
+ };
30
+
31
+ type AnyChangesToRecordSubscription = {
32
+ readonly kind: 'AnyChangesToRecord';
33
+ readonly callback: () => void;
34
+ readonly recordLink: Link;
35
+ };
36
+
37
+ type AnyRecordSubscription = {
38
+ readonly kind: 'AnyRecords';
39
+ readonly callback: () => void;
40
+ };
41
+
42
+ type Subscription =
43
+ | FragmentSubscription<{ parameters: object; data: object }>
44
+ | AnyChangesToRecordSubscription
45
+ | AnyRecordSubscription;
46
+ type Subscriptions = Set<Subscription>;
47
+ // Should this be a map?
48
+ type CacheMap<T> = { [index: string]: ParentCache<T> };
49
+
50
+ export type IsographEnvironment = {
51
+ readonly store: IsographStore;
52
+ readonly networkFunction: IsographNetworkFunction;
53
+ readonly missingFieldHandler: MissingFieldHandler | null;
54
+ readonly componentCache: ComponentCache;
55
+ readonly subscriptions: Subscriptions;
56
+ // N.B. this must be <any, any>, but all *usages* of this should go through
57
+ // a function that adds type parameters.
58
+ readonly fragmentCache: CacheMap<FragmentReference<any, any>>;
59
+ // TODO make this a CacheMap and add GC
60
+ readonly entrypointArtifactCache: Map<
61
+ string,
62
+ PromiseWrapper<IsographEntrypoint<any, any>>
63
+ >;
64
+ readonly retainedQueries: Set<RetainedQuery>;
65
+ readonly gcBuffer: Array<RetainedQuery>;
66
+ readonly gcBufferSize: number;
67
+ readonly loggers: Set<WrappedLogFunction>;
68
+ };
69
+
70
+ export type MissingFieldHandler = (
71
+ storeRecord: StoreRecord,
72
+ root: Link,
73
+ fieldName: string,
74
+ arguments_: { [index: string]: any } | null,
75
+ variables: Variables | null,
76
+ ) => Link | undefined;
77
+
78
+ export type IsographNetworkFunction = (
79
+ queryText: string,
80
+ variables: Variables,
81
+ ) => Promise<any>;
82
+
83
+ export type Link = {
84
+ readonly __link: DataId;
85
+ readonly __typename: TypeName;
86
+ };
87
+
88
+ export type DataTypeValue =
89
+ // N.B. undefined is here to support optional id's, but
90
+ // undefined should not *actually* be present in the store.
91
+ | undefined
92
+ // Singular scalar fields:
93
+ | number
94
+ | boolean
95
+ | string
96
+ | null
97
+ // Singular linked fields:
98
+ | Link
99
+ // Plural scalar and linked fields:
100
+ | DataTypeValue[];
101
+
102
+ export type StoreRecord = {
103
+ [index: DataId | string]: DataTypeValue;
104
+ // TODO __typename?: T, which is restricted to being a concrete string
105
+ // TODO this shouldn't always be named id
106
+ readonly id?: DataId;
107
+ };
108
+
109
+ export type TypeName = string;
110
+ export type DataId = string;
111
+
112
+ export const ROOT_ID: DataId & '__ROOT' = '__ROOT';
113
+
114
+ export type IsographStore = {
115
+ [index: TypeName]: {
116
+ [index: DataId]: StoreRecord | null;
117
+ } | null;
118
+ readonly Query: {
119
+ readonly __ROOT: StoreRecord;
120
+ };
121
+ };
122
+
123
+ const DEFAULT_GC_BUFFER_SIZE = 10;
124
+ export function createIsographEnvironment(
125
+ store: IsographStore,
126
+ networkFunction: IsographNetworkFunction,
127
+ missingFieldHandler?: MissingFieldHandler | null,
128
+ logFunction?: LogFunction | null,
129
+ ): IsographEnvironment {
130
+ return {
131
+ store,
132
+ networkFunction,
133
+ missingFieldHandler: missingFieldHandler ?? null,
134
+ componentCache: {},
135
+ subscriptions: new Set(),
136
+ fragmentCache: {},
137
+ entrypointArtifactCache: new Map(),
138
+ retainedQueries: new Set(),
139
+ gcBuffer: [],
140
+ gcBufferSize: DEFAULT_GC_BUFFER_SIZE,
141
+ loggers: logFunction != null ? new Set([{ log: logFunction }]) : new Set(),
142
+ };
143
+ }
144
+
145
+ export function createIsographStore(): IsographStore {
146
+ return {
147
+ Query: {
148
+ [ROOT_ID]: {},
149
+ },
150
+ };
151
+ }
152
+
153
+ export function assertLink(link: DataTypeValue): Link | null | undefined {
154
+ if (Array.isArray(link)) {
155
+ throw new Error('Unexpected array');
156
+ }
157
+ if (link == null) {
158
+ return link;
159
+ }
160
+ if (typeof link === 'object') {
161
+ return link;
162
+ }
163
+ throw new Error('Invalid link');
164
+ }
165
+
166
+ export function getLink(maybeLink: DataTypeValue): Link | null {
167
+ if (
168
+ maybeLink != null &&
169
+ typeof maybeLink === 'object' &&
170
+ '__link' in maybeLink &&
171
+ maybeLink.__link != null &&
172
+ '__typename' in maybeLink &&
173
+ maybeLink.__typename != null
174
+ ) {
175
+ return maybeLink;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ export function getOrLoadIsographArtifact(
181
+ environment: IsographEnvironment,
182
+ key: string,
183
+ loader: () => Promise<IsographEntrypoint<any, any>>,
184
+ ): PromiseWrapper<IsographEntrypoint<any, any>> {
185
+ const value = environment.entrypointArtifactCache.get(key);
186
+ if (value != null) {
187
+ return value;
188
+ }
189
+ const wrapped = wrapPromise(loader());
190
+ environment.entrypointArtifactCache.set(key, wrapped);
191
+ return wrapped;
192
+ }