@isograph/react 0.1.0 → 0.2.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.
- package/dist/core/FragmentReference.d.ts +15 -0
- package/dist/core/FragmentReference.js +17 -0
- package/dist/core/IsographEnvironment.d.ts +71 -0
- package/dist/core/IsographEnvironment.js +72 -0
- package/dist/core/PromiseWrapper.d.ts +27 -0
- package/dist/core/PromiseWrapper.js +58 -0
- package/dist/core/areEqualWithDeepComparison.d.ts +3 -0
- package/dist/core/areEqualWithDeepComparison.js +61 -0
- package/dist/core/cache.d.ts +28 -0
- package/dist/core/cache.js +452 -0
- package/dist/core/componentCache.d.ts +5 -0
- package/dist/core/componentCache.js +38 -0
- package/dist/core/entrypoint.d.ts +50 -0
- package/dist/core/entrypoint.js +8 -0
- package/dist/core/garbageCollection.d.ts +11 -0
- package/dist/core/garbageCollection.js +74 -0
- package/dist/core/makeNetworkRequest.d.ts +6 -0
- package/dist/core/makeNetworkRequest.js +62 -0
- package/dist/core/read.d.ts +12 -0
- package/dist/core/read.js +415 -0
- package/dist/core/reader.d.ts +63 -0
- package/dist/core/reader.js +2 -0
- package/dist/core/util.d.ts +17 -0
- package/dist/core/util.js +2 -0
- package/dist/index.d.ts +21 -118
- package/dist/index.js +50 -303
- package/dist/loadable-hooks/useClientSideDefer.d.ts +4 -0
- package/dist/loadable-hooks/useClientSideDefer.js +15 -0
- package/dist/loadable-hooks/useImperativeExposedMutationField.d.ts +5 -0
- package/dist/loadable-hooks/useImperativeExposedMutationField.js +15 -0
- package/dist/loadable-hooks/useImperativeLoadableField.d.ts +9 -0
- package/dist/loadable-hooks/useImperativeLoadableField.js +15 -0
- package/dist/loadable-hooks/useSkipLimitPagination.d.ts +33 -0
- package/dist/loadable-hooks/useSkipLimitPagination.js +118 -0
- package/dist/react/FragmentReader.d.ts +13 -0
- package/dist/react/FragmentReader.js +33 -0
- package/dist/react/IsographEnvironmentProvider.d.ts +10 -0
- package/dist/{IsographEnvironment.js → react/IsographEnvironmentProvider.js} +2 -20
- package/dist/react/useImperativeReference.d.ts +7 -0
- package/dist/react/useImperativeReference.js +36 -0
- package/dist/react/useLazyReference.d.ts +5 -0
- package/dist/react/useLazyReference.js +14 -0
- package/dist/react/useReadAndSubscribe.d.ts +11 -0
- package/dist/react/useReadAndSubscribe.js +41 -0
- package/dist/react/useRerenderOnChange.d.ts +3 -0
- package/dist/react/useRerenderOnChange.js +23 -0
- package/dist/react/useResult.d.ts +5 -0
- package/dist/react/useResult.js +36 -0
- package/docs/how-useLazyReference-works.md +117 -0
- package/package.json +12 -6
- package/src/core/FragmentReference.ts +37 -0
- package/src/core/IsographEnvironment.ts +183 -0
- package/src/core/PromiseWrapper.ts +86 -0
- package/src/core/areEqualWithDeepComparison.ts +78 -0
- package/src/core/cache.ts +721 -0
- package/src/core/componentCache.ts +61 -0
- package/src/core/entrypoint.ts +99 -0
- package/src/core/garbageCollection.ts +122 -0
- package/src/core/makeNetworkRequest.ts +99 -0
- package/src/core/read.ts +615 -0
- package/src/core/reader.ts +133 -0
- package/src/core/util.ts +23 -0
- package/src/index.ts +86 -0
- package/src/loadable-hooks/useClientSideDefer.ts +28 -0
- package/src/loadable-hooks/useImperativeExposedMutationField.ts +17 -0
- package/src/loadable-hooks/useImperativeLoadableField.ts +26 -0
- package/src/loadable-hooks/useSkipLimitPagination.ts +211 -0
- package/src/react/FragmentReader.tsx +34 -0
- package/src/react/IsographEnvironmentProvider.tsx +33 -0
- package/src/react/useImperativeReference.ts +57 -0
- package/src/react/useLazyReference.ts +22 -0
- package/src/react/useReadAndSubscribe.ts +66 -0
- package/src/react/useRerenderOnChange.ts +33 -0
- package/src/react/useResult.ts +65 -0
- package/src/tests/__isograph/Query/meName/entrypoint.ts +47 -0
- package/src/tests/__isograph/Query/meName/output_type.ts +3 -0
- package/src/tests/__isograph/Query/meName/param_type.ts +6 -0
- package/src/tests/__isograph/Query/meName/resolver_reader.ts +32 -0
- package/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +83 -0
- package/src/tests/__isograph/Query/meNameSuccessor/output_type.ts +3 -0
- package/src/tests/__isograph/Query/meNameSuccessor/param_type.ts +11 -0
- package/src/tests/__isograph/Query/meNameSuccessor/resolver_reader.ts +54 -0
- package/src/tests/__isograph/Query/nodeField/entrypoint.ts +46 -0
- package/src/tests/__isograph/Query/nodeField/output_type.ts +3 -0
- package/src/tests/__isograph/Query/nodeField/param_type.ts +6 -0
- package/src/tests/__isograph/Query/nodeField/resolver_reader.ts +37 -0
- package/src/tests/__isograph/iso.ts +88 -0
- package/src/tests/garbageCollection.test.ts +136 -0
- package/src/tests/isograph.config.json +7 -0
- package/src/tests/meNameSuccessor.ts +20 -0
- package/src/tests/nodeQuery.ts +17 -0
- package/src/tests/schema.graphql +16 -0
- package/src/tests/tsconfig.json +21 -0
- package/tsconfig.json +6 -0
- package/tsconfig.pkg.json +2 -1
- package/dist/IsographEnvironment.d.ts +0 -56
- package/dist/PromiseWrapper.d.ts +0 -13
- package/dist/PromiseWrapper.js +0 -22
- package/dist/cache.d.ts +0 -26
- package/dist/cache.js +0 -274
- package/dist/componentCache.d.ts +0 -6
- package/dist/componentCache.js +0 -31
- package/src/IsographEnvironment.tsx +0 -120
- package/src/PromiseWrapper.ts +0 -29
- package/src/cache.tsx +0 -484
- package/src/componentCache.ts +0 -44
- package/src/index.tsx +0 -651
@@ -0,0 +1,118 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useSkipLimitPagination = void 0;
|
4
|
+
const IsographEnvironmentProvider_1 = require("../react/IsographEnvironmentProvider");
|
5
|
+
const useResult_1 = require("../react/useResult");
|
6
|
+
const read_1 = require("../core/read");
|
7
|
+
const react_disposable_state_1 = require("@isograph/react-disposable-state");
|
8
|
+
const reference_counted_pointer_1 = require("@isograph/reference-counted-pointer");
|
9
|
+
const PromiseWrapper_1 = require("../core/PromiseWrapper");
|
10
|
+
function flatten(arr) {
|
11
|
+
let outArray = [];
|
12
|
+
for (const subarr of arr) {
|
13
|
+
for (const item of subarr) {
|
14
|
+
outArray.push(item);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
return outArray;
|
18
|
+
}
|
19
|
+
/**
|
20
|
+
* accepts a loadableField that accepts skip and limit arguments
|
21
|
+
* and returns:
|
22
|
+
* - a fetchMore function that, when called, triggers a network
|
23
|
+
* request for additional data, and
|
24
|
+
* - the data received so far.
|
25
|
+
*
|
26
|
+
* This hook will suspend if any network request is in flight.
|
27
|
+
*
|
28
|
+
* Calling fetchMore before the hook mounts is a no-op.
|
29
|
+
*
|
30
|
+
* NOTE: this hook does not subscribe to changes. This is a known
|
31
|
+
* issue. If you are running into this issue, reach out on GitHub/
|
32
|
+
* Twitter, and we'll fix the issue.
|
33
|
+
*/
|
34
|
+
function useSkipLimitPagination(loadableField) {
|
35
|
+
const networkRequestOptions = {
|
36
|
+
suspendIfInFlight: true,
|
37
|
+
throwOnNetworkError: true,
|
38
|
+
};
|
39
|
+
const { state, setState } = (0, react_disposable_state_1.useUpdatableDisposableState)();
|
40
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
41
|
+
function readCompletedFragmentReferences(completedReferences) {
|
42
|
+
// In general, this will not suspend. But it could, if there is missing data.
|
43
|
+
// A better version of this hook would not do any reading here.
|
44
|
+
const results = completedReferences.map(([pointer]) => {
|
45
|
+
const fragmentReference = pointer.getItemIfNotDisposed();
|
46
|
+
if (fragmentReference == null) {
|
47
|
+
throw new Error('FragmentReference is unexpectedly disposed. \
|
48
|
+
This is indicative of a bug in Isograph.');
|
49
|
+
}
|
50
|
+
(0, useResult_1.maybeUnwrapNetworkRequest)(fragmentReference.networkRequest, networkRequestOptions);
|
51
|
+
const data = (0, read_1.readButDoNotEvaluate)(environment, fragmentReference, networkRequestOptions);
|
52
|
+
const readerWithRefetchQueries = (0, PromiseWrapper_1.readPromise)(fragmentReference.readerWithRefetchQueries);
|
53
|
+
return readerWithRefetchQueries.readerArtifact.resolver(data.item, undefined);
|
54
|
+
});
|
55
|
+
const items = flatten(results);
|
56
|
+
return items;
|
57
|
+
}
|
58
|
+
const getFetchMore = (loadedSoFar) => (args, count) => {
|
59
|
+
// @ts-expect-error
|
60
|
+
const loadedField = loadableField(Object.assign(Object.assign({}, args), { skip: loadedSoFar, limit: count }))[1]();
|
61
|
+
const newPointer = (0, reference_counted_pointer_1.createReferenceCountedPointer)(loadedField);
|
62
|
+
const clonedPointers = loadedReferences.map(([refCountedPointer]) => {
|
63
|
+
const clonedRefCountedPointer = refCountedPointer.cloneIfNotDisposed();
|
64
|
+
if (clonedRefCountedPointer == null) {
|
65
|
+
throw new Error('This reference counted pointer has already been disposed. \
|
66
|
+
This is indicative of a bug in useSkipLimitPagination.');
|
67
|
+
}
|
68
|
+
return clonedRefCountedPointer;
|
69
|
+
});
|
70
|
+
clonedPointers.push(newPointer);
|
71
|
+
const totalItemCleanupPair = [
|
72
|
+
clonedPointers,
|
73
|
+
() => {
|
74
|
+
clonedPointers.forEach(([, dispose]) => {
|
75
|
+
dispose();
|
76
|
+
});
|
77
|
+
},
|
78
|
+
];
|
79
|
+
setState(totalItemCleanupPair);
|
80
|
+
};
|
81
|
+
const loadedReferences = state === react_disposable_state_1.UNASSIGNED_STATE ? [] : state;
|
82
|
+
if (loadedReferences.length === 0) {
|
83
|
+
return {
|
84
|
+
kind: 'Complete',
|
85
|
+
fetchMore: getFetchMore(0),
|
86
|
+
results: [],
|
87
|
+
};
|
88
|
+
}
|
89
|
+
const mostRecentItem = loadedReferences[loadedReferences.length - 1];
|
90
|
+
const mostRecentFragmentReference = mostRecentItem[0].getItemIfNotDisposed();
|
91
|
+
if (mostRecentFragmentReference === null) {
|
92
|
+
throw new Error('FragmentReference is unexpectedly disposed. \
|
93
|
+
This is indicative of a bug in Isograph.');
|
94
|
+
}
|
95
|
+
const networkRequestStatus = (0, PromiseWrapper_1.getPromiseState)(mostRecentFragmentReference.networkRequest);
|
96
|
+
switch (networkRequestStatus.kind) {
|
97
|
+
case 'Pending': {
|
98
|
+
const completedFragmentReferences = loadedReferences.slice(0, loadedReferences.length - 1);
|
99
|
+
return {
|
100
|
+
kind: 'Pending',
|
101
|
+
pendingFragment: mostRecentFragmentReference,
|
102
|
+
results: readCompletedFragmentReferences(completedFragmentReferences),
|
103
|
+
};
|
104
|
+
}
|
105
|
+
case 'Err': {
|
106
|
+
throw networkRequestStatus.error;
|
107
|
+
}
|
108
|
+
case 'Ok': {
|
109
|
+
const results = readCompletedFragmentReferences(loadedReferences);
|
110
|
+
return {
|
111
|
+
kind: 'Complete',
|
112
|
+
results,
|
113
|
+
fetchMore: getFetchMore(results.length),
|
114
|
+
};
|
115
|
+
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
exports.useSkipLimitPagination = useSkipLimitPagination;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import * as React from 'react';
|
2
|
+
import { ExtractReadFromStore, IsographEntrypoint } from '../core/entrypoint';
|
3
|
+
import { FragmentReference } from '../core/FragmentReference';
|
4
|
+
import { NetworkRequestReaderOptions } from '../core/read';
|
5
|
+
export declare function FragmentReader<TProps extends Record<any, any>, TEntrypoint extends IsographEntrypoint<any, React.FC<TProps>>>(props: TProps extends Record<string, never> ? {
|
6
|
+
fragmentReference: FragmentReference<ExtractReadFromStore<TEntrypoint>, React.FC<{}>>;
|
7
|
+
additionalProps?: TProps;
|
8
|
+
networkRequestOptions?: Partial<NetworkRequestReaderOptions>;
|
9
|
+
} : {
|
10
|
+
fragmentReference: FragmentReference<ExtractReadFromStore<TEntrypoint>, React.FC<TProps>>;
|
11
|
+
additionalProps: TProps;
|
12
|
+
networkRequestOptions?: Partial<NetworkRequestReaderOptions>;
|
13
|
+
}): React.ReactNode;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
19
|
+
if (mod && mod.__esModule) return mod;
|
20
|
+
var result = {};
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
22
|
+
__setModuleDefault(result, mod);
|
23
|
+
return result;
|
24
|
+
};
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
26
|
+
exports.FragmentReader = void 0;
|
27
|
+
const React = __importStar(require("react"));
|
28
|
+
const useResult_1 = require("./useResult");
|
29
|
+
function FragmentReader(props) {
|
30
|
+
const Component = (0, useResult_1.useResult)(props.fragmentReference, props.networkRequestOptions);
|
31
|
+
return React.createElement(Component, Object.assign({}, props.additionalProps));
|
32
|
+
}
|
33
|
+
exports.FragmentReader = FragmentReader;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import * as React from 'react';
|
2
|
+
import { ReactNode } from 'react';
|
3
|
+
import { type IsographEnvironment } from '../core/IsographEnvironment';
|
4
|
+
export declare const IsographEnvironmentContext: React.Context<IsographEnvironment | null>;
|
5
|
+
export type IsographEnvironmentProviderProps = {
|
6
|
+
readonly environment: IsographEnvironment;
|
7
|
+
readonly children: ReactNode;
|
8
|
+
};
|
9
|
+
export declare function IsographEnvironmentProvider({ environment, children, }: IsographEnvironmentProviderProps): React.ReactElement;
|
10
|
+
export declare function useIsographEnvironment(): IsographEnvironment;
|
@@ -23,11 +23,10 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
23
23
|
return result;
|
24
24
|
};
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
26
|
-
exports.
|
27
|
-
const react_1 = require("react");
|
26
|
+
exports.useIsographEnvironment = exports.IsographEnvironmentProvider = exports.IsographEnvironmentContext = void 0;
|
28
27
|
const React = __importStar(require("react"));
|
28
|
+
const react_1 = require("react");
|
29
29
|
exports.IsographEnvironmentContext = (0, react_1.createContext)(null);
|
30
|
-
exports.ROOT_ID = '__ROOT';
|
31
30
|
function IsographEnvironmentProvider({ environment, children, }) {
|
32
31
|
return (React.createElement(exports.IsographEnvironmentContext.Provider, { value: environment }, children));
|
33
32
|
}
|
@@ -41,20 +40,3 @@ function useIsographEnvironment() {
|
|
41
40
|
return context;
|
42
41
|
}
|
43
42
|
exports.useIsographEnvironment = useIsographEnvironment;
|
44
|
-
function createIsographEnvironment(store, networkFunction, missingFieldHandler) {
|
45
|
-
return {
|
46
|
-
store,
|
47
|
-
networkFunction,
|
48
|
-
missingFieldHandler: missingFieldHandler !== null && missingFieldHandler !== void 0 ? missingFieldHandler : null,
|
49
|
-
componentCache: {},
|
50
|
-
subscriptions: new Set(),
|
51
|
-
suspenseCache: {},
|
52
|
-
};
|
53
|
-
}
|
54
|
-
exports.createIsographEnvironment = createIsographEnvironment;
|
55
|
-
function createIsographStore() {
|
56
|
-
return {
|
57
|
-
[exports.ROOT_ID]: {},
|
58
|
-
};
|
59
|
-
}
|
60
|
-
exports.createIsographStore = createIsographStore;
|
@@ -0,0 +1,7 @@
|
|
1
|
+
import { UnassignedState } from '@isograph/react-disposable-state';
|
2
|
+
import { IsographEntrypoint } from '../core/entrypoint';
|
3
|
+
import { FragmentReference, Variables } from '../core/FragmentReference';
|
4
|
+
export declare function useImperativeReference<TReadFromStore extends Object, TClientFieldValue>(entrypoint: IsographEntrypoint<TReadFromStore, TClientFieldValue>): {
|
5
|
+
fragmentReference: FragmentReference<TReadFromStore, TClientFieldValue> | UnassignedState;
|
6
|
+
loadFragmentReference: (variables: Variables) => void;
|
7
|
+
};
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useImperativeReference = void 0;
|
4
|
+
const react_disposable_state_1 = require("@isograph/react-disposable-state");
|
5
|
+
const IsographEnvironmentProvider_1 = require("./IsographEnvironmentProvider");
|
6
|
+
const IsographEnvironment_1 = require("../core/IsographEnvironment");
|
7
|
+
const makeNetworkRequest_1 = require("../core/makeNetworkRequest");
|
8
|
+
const PromiseWrapper_1 = require("../core/PromiseWrapper");
|
9
|
+
// TODO rename this to useImperativelyLoadedEntrypoint
|
10
|
+
function useImperativeReference(entrypoint) {
|
11
|
+
const { state, setState } = (0, react_disposable_state_1.useUpdatableDisposableState)();
|
12
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
13
|
+
return {
|
14
|
+
fragmentReference: state,
|
15
|
+
loadFragmentReference: (variables) => {
|
16
|
+
const [networkRequest, disposeNetworkRequest] = (0, makeNetworkRequest_1.makeNetworkRequest)(environment, entrypoint, variables);
|
17
|
+
setState([
|
18
|
+
{
|
19
|
+
kind: 'FragmentReference',
|
20
|
+
readerWithRefetchQueries: (0, PromiseWrapper_1.wrapResolvedValue)({
|
21
|
+
kind: 'ReaderWithRefetchQueries',
|
22
|
+
readerArtifact: entrypoint.readerWithRefetchQueries.readerArtifact,
|
23
|
+
nestedRefetchQueries: entrypoint.readerWithRefetchQueries.nestedRefetchQueries,
|
24
|
+
}),
|
25
|
+
root: IsographEnvironment_1.ROOT_ID,
|
26
|
+
variables,
|
27
|
+
networkRequest,
|
28
|
+
},
|
29
|
+
() => {
|
30
|
+
disposeNetworkRequest();
|
31
|
+
},
|
32
|
+
]);
|
33
|
+
},
|
34
|
+
};
|
35
|
+
}
|
36
|
+
exports.useImperativeReference = useImperativeReference;
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { FragmentReference, Variables } from '../core/FragmentReference';
|
2
|
+
import { IsographEntrypoint } from '../core/entrypoint';
|
3
|
+
export declare function useLazyReference<TReadFromStore extends Object, TClientFieldValue>(entrypoint: IsographEntrypoint<TReadFromStore, TClientFieldValue>, variables: Variables): {
|
4
|
+
fragmentReference: FragmentReference<TReadFromStore, TClientFieldValue>;
|
5
|
+
};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useLazyReference = void 0;
|
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
|
+
function useLazyReference(entrypoint, variables) {
|
8
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
9
|
+
const cache = (0, cache_1.getOrCreateCacheForArtifact)(environment, entrypoint, variables);
|
10
|
+
return {
|
11
|
+
fragmentReference: (0, react_disposable_state_1.useLazyDisposableState)(cache).state,
|
12
|
+
};
|
13
|
+
}
|
14
|
+
exports.useLazyReference = useLazyReference;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { FragmentReference } from '../core/FragmentReference';
|
2
|
+
import { NetworkRequestReaderOptions, WithEncounteredRecords } from '../core/read';
|
3
|
+
/**
|
4
|
+
* Read the data from a fragment reference and subscribe to updates.
|
5
|
+
*/
|
6
|
+
export declare function useReadAndSubscribe<TReadFromStore extends Object>(fragmentReference: FragmentReference<TReadFromStore, any>, networkRequestOptions: NetworkRequestReaderOptions): TReadFromStore;
|
7
|
+
export declare function useSubscribeToMultiple<TReadFromStore extends Object>(items: ReadonlyArray<{
|
8
|
+
records: WithEncounteredRecords<TReadFromStore>;
|
9
|
+
callback: (updatedRecords: WithEncounteredRecords<TReadFromStore>) => void;
|
10
|
+
fragmentReference: FragmentReference<TReadFromStore, any>;
|
11
|
+
}>): void;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useSubscribeToMultiple = exports.useReadAndSubscribe = void 0;
|
4
|
+
const react_1 = require("react");
|
5
|
+
const FragmentReference_1 = require("../core/FragmentReference");
|
6
|
+
const read_1 = require("../core/read");
|
7
|
+
const useRerenderOnChange_1 = require("./useRerenderOnChange");
|
8
|
+
const IsographEnvironmentProvider_1 = require("./IsographEnvironmentProvider");
|
9
|
+
const cache_1 = require("../core/cache");
|
10
|
+
/**
|
11
|
+
* Read the data from a fragment reference and subscribe to updates.
|
12
|
+
*/
|
13
|
+
function useReadAndSubscribe(fragmentReference, networkRequestOptions) {
|
14
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
15
|
+
const [readOutDataAndRecords, setReadOutDataAndRecords] = (0, react_1.useState)(() => (0, read_1.readButDoNotEvaluate)(environment, fragmentReference, networkRequestOptions));
|
16
|
+
(0, useRerenderOnChange_1.useRerenderOnChange)(readOutDataAndRecords, fragmentReference, setReadOutDataAndRecords);
|
17
|
+
return readOutDataAndRecords.item;
|
18
|
+
}
|
19
|
+
exports.useReadAndSubscribe = useReadAndSubscribe;
|
20
|
+
function useSubscribeToMultiple(items) {
|
21
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
22
|
+
(0, react_1.useEffect)(() => {
|
23
|
+
const cleanupFns = items.map(({ records, callback, fragmentReference }) => {
|
24
|
+
return (0, cache_1.subscribe)(environment, records, fragmentReference, callback);
|
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
|
+
}
|
41
|
+
exports.useSubscribeToMultiple = useSubscribeToMultiple;
|
@@ -0,0 +1,3 @@
|
|
1
|
+
import { WithEncounteredRecords } from '../core/read';
|
2
|
+
import { FragmentReference } from '../core/FragmentReference';
|
3
|
+
export declare function useRerenderOnChange<TReadFromStore extends Object>(encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>, fragmentReference: FragmentReference<any, any>, setEncounteredDataAndRecords: (data: WithEncounteredRecords<TReadFromStore>) => void): void;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useRerenderOnChange = void 0;
|
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) {
|
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
|
+
});
|
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
|
+
}
|
23
|
+
exports.useRerenderOnChange = useRerenderOnChange;
|
@@ -0,0 +1,5 @@
|
|
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 Object, TClientFieldValue>(fragmentReference: FragmentReference<TReadFromStore, TClientFieldValue>, partialNetworkRequestOptions?: Partial<NetworkRequestReaderOptions> | void): TClientFieldValue;
|
5
|
+
export declare function maybeUnwrapNetworkRequest(networkRequest: PromiseWrapper<void, any>, networkRequestOptions: NetworkRequestReaderOptions): void;
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.maybeUnwrapNetworkRequest = exports.useResult = void 0;
|
4
|
+
const IsographEnvironmentProvider_1 = require("../react/IsographEnvironmentProvider");
|
5
|
+
const componentCache_1 = require("../core/componentCache");
|
6
|
+
const useReadAndSubscribe_1 = require("./useReadAndSubscribe");
|
7
|
+
const read_1 = require("../core/read");
|
8
|
+
const PromiseWrapper_1 = require("../core/PromiseWrapper");
|
9
|
+
function useResult(fragmentReference, partialNetworkRequestOptions) {
|
10
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
11
|
+
const networkRequestOptions = (0, read_1.getNetworkRequestOptionsWithDefaults)(partialNetworkRequestOptions);
|
12
|
+
maybeUnwrapNetworkRequest(fragmentReference.networkRequest, networkRequestOptions);
|
13
|
+
const readerWithRefetchQueries = (0, PromiseWrapper_1.readPromise)(fragmentReference.readerWithRefetchQueries);
|
14
|
+
switch (readerWithRefetchQueries.readerArtifact.kind) {
|
15
|
+
case 'ComponentReaderArtifact': {
|
16
|
+
// @ts-expect-error
|
17
|
+
return (0, componentCache_1.getOrCreateCachedComponent)(environment, readerWithRefetchQueries.readerArtifact.componentName, fragmentReference, networkRequestOptions);
|
18
|
+
}
|
19
|
+
case 'EagerReaderArtifact': {
|
20
|
+
const data = (0, useReadAndSubscribe_1.useReadAndSubscribe)(fragmentReference, networkRequestOptions);
|
21
|
+
return readerWithRefetchQueries.readerArtifact.resolver(data);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
exports.useResult = useResult;
|
26
|
+
function maybeUnwrapNetworkRequest(networkRequest, networkRequestOptions) {
|
27
|
+
const state = (0, PromiseWrapper_1.getPromiseState)(networkRequest);
|
28
|
+
if (state.kind === 'Err' && networkRequestOptions.throwOnNetworkError) {
|
29
|
+
throw state.error;
|
30
|
+
}
|
31
|
+
else if (state.kind === 'Pending' &&
|
32
|
+
networkRequestOptions.suspendIfInFlight) {
|
33
|
+
throw state.promise;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
exports.maybeUnwrapNetworkRequest = maybeUnwrapNetworkRequest;
|
@@ -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.
|
package/package.json
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
{
|
2
2
|
"name": "@isograph/react",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"description": "Use Isograph with React",
|
5
|
+
"homepage": "https://isograph.dev",
|
5
6
|
"main": "dist/index.js",
|
6
7
|
"types": "dist/index.d.ts",
|
7
8
|
"author": "Isograph Labs",
|
@@ -9,15 +10,19 @@
|
|
9
10
|
"scripts": {
|
10
11
|
"compile": "rm -rf dist/* && tsc -p tsconfig.pkg.json",
|
11
12
|
"compile-watch": "tsc -p tsconfig.pkg.json --watch",
|
12
|
-
"test": "
|
13
|
+
"test": "vitest run",
|
13
14
|
"test-watch": "vitest watch",
|
14
15
|
"coverage": "vitest run --coverage",
|
15
|
-
"prepack": "yarn run test && yarn run compile"
|
16
|
+
"prepack": "yarn run test && yarn run compile",
|
17
|
+
"tsc": "tsc"
|
16
18
|
},
|
17
19
|
"dependencies": {
|
18
|
-
"@isograph/disposable-types": "0.
|
20
|
+
"@isograph/disposable-types": "0.2.0",
|
19
21
|
"@isograph/react-disposable-state": "*",
|
20
|
-
"
|
22
|
+
"@isograph/reference-counted-pointer": "*"
|
23
|
+
},
|
24
|
+
"peerDependencies": {
|
25
|
+
"react": "18.2.0"
|
21
26
|
},
|
22
27
|
"devDependencies": {
|
23
28
|
"@types/react": "^18.0.31",
|
@@ -29,5 +34,6 @@
|
|
29
34
|
"type": "git",
|
30
35
|
"url": "git+https://github.com/isographlabs/isograph.git",
|
31
36
|
"directory": "libs/isograph-react"
|
32
|
-
}
|
37
|
+
},
|
38
|
+
"sideEffects": false
|
33
39
|
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { DataId } 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 FragmentReference<
|
11
|
+
TReadFromStore extends Object,
|
12
|
+
TClientFieldValue,
|
13
|
+
> = {
|
14
|
+
readonly kind: 'FragmentReference';
|
15
|
+
readonly readerWithRefetchQueries: PromiseWrapper<
|
16
|
+
ReaderWithRefetchQueries<TReadFromStore, TClientFieldValue>
|
17
|
+
>;
|
18
|
+
readonly root: DataId;
|
19
|
+
readonly variables: Variables | null;
|
20
|
+
readonly networkRequest: PromiseWrapper<void, any>;
|
21
|
+
};
|
22
|
+
|
23
|
+
export function stableIdForFragmentReference(
|
24
|
+
fragmentReference: FragmentReference<any, any>,
|
25
|
+
): string {
|
26
|
+
return `${fragmentReference.root}/TODO_FRAGMENT_NAME/${serializeVariables(fragmentReference.variables ?? {})}`;
|
27
|
+
}
|
28
|
+
|
29
|
+
function serializeVariables(variables: Variables) {
|
30
|
+
let s = '';
|
31
|
+
const keys = Object.keys(variables);
|
32
|
+
keys.sort();
|
33
|
+
for (const key of keys) {
|
34
|
+
s += `${key}:${variables[key]},`;
|
35
|
+
}
|
36
|
+
return s;
|
37
|
+
}
|