@isograph/react 0.0.0-main-d3ef6e33 → 0.0.0-main-6c4d9bd8
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 +1 -0
- package/dist/core/FragmentReference.js +15 -0
- package/dist/core/IsographEnvironment.d.ts +1 -1
- package/dist/core/cache.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -1
- package/dist/loadable-hooks/useSuspensefulSkipLimitPagination.d.ts +23 -0
- package/dist/loadable-hooks/useSuspensefulSkipLimitPagination.js +78 -0
- package/dist/react/useReadAndSubscribe.d.ts +6 -2
- package/dist/react/useReadAndSubscribe.js +25 -2
- package/package.json +4 -3
- package/src/core/FragmentReference.ts +16 -0
- package/src/core/IsographEnvironment.ts +1 -1
- package/src/core/cache.ts +5 -2
- package/src/index.ts +6 -0
- package/src/loadable-hooks/useSuspensefulSkipLimitPagination.ts +147 -0
- package/src/react/useReadAndSubscribe.ts +41 -3
@@ -14,3 +14,4 @@ export type FragmentReference<TReadFromStore extends Object, TClientFieldValue>
|
|
14
14
|
readonly nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[];
|
15
15
|
readonly networkRequest: PromiseWrapper<void, any>;
|
16
16
|
};
|
17
|
+
export declare function stableIdForFragmentReference(fragmentReference: FragmentReference<any, any>): string;
|
@@ -1,2 +1,17 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.stableIdForFragmentReference = void 0;
|
4
|
+
function stableIdForFragmentReference(fragmentReference) {
|
5
|
+
var _a;
|
6
|
+
return `${fragmentReference.root}/TODO_FRAGMENT_NAME/${serializeVariables((_a = fragmentReference.variables) !== null && _a !== void 0 ? _a : {})}`;
|
7
|
+
}
|
8
|
+
exports.stableIdForFragmentReference = stableIdForFragmentReference;
|
9
|
+
function serializeVariables(variables) {
|
10
|
+
let s = '';
|
11
|
+
const keys = Object.keys(variables);
|
12
|
+
keys.sort();
|
13
|
+
for (const key of keys) {
|
14
|
+
s += `${key}:${variables[key]},`;
|
15
|
+
}
|
16
|
+
return s;
|
17
|
+
}
|
@@ -12,7 +12,7 @@ type ComponentCache = {
|
|
12
12
|
};
|
13
13
|
};
|
14
14
|
};
|
15
|
-
type FragmentSubscription<TReadFromStore extends Object> = {
|
15
|
+
export type FragmentSubscription<TReadFromStore extends Object> = {
|
16
16
|
readonly kind: 'FragmentSubscription';
|
17
17
|
readonly callback: (newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>) => void;
|
18
18
|
/** The value read out from the previous call to readButDoNotEvaluate */
|
package/dist/core/cache.js
CHANGED
@@ -59,6 +59,7 @@ function normalizeData(environment, normalizationAst, networkResponse, variables
|
|
59
59
|
console.log('after normalization', {
|
60
60
|
store: environment.store,
|
61
61
|
encounteredIds,
|
62
|
+
environment,
|
62
63
|
});
|
63
64
|
}
|
64
65
|
callSubscriptions(environment, encounteredIds);
|
@@ -74,6 +75,7 @@ function subscribeToAnyChange(environment, callback) {
|
|
74
75
|
return () => environment.subscriptions.delete(subscription);
|
75
76
|
}
|
76
77
|
exports.subscribeToAnyChange = subscribeToAnyChange;
|
78
|
+
// TODO we should re-read and call callback if the value has changed
|
77
79
|
function subscribe(environment, encounteredDataAndRecords, fragmentReference, callback) {
|
78
80
|
const fragmentSubscription = {
|
79
81
|
kind: 'FragmentSubscription',
|
package/dist/index.d.ts
CHANGED
@@ -7,11 +7,13 @@ export { type EagerReaderArtifact, type ComponentReaderArtifact, type RefetchRea
|
|
7
7
|
export { type NormalizationAst, type NormalizationAstNode, type NormalizationLinkedField, type NormalizationScalarField, type IsographEntrypoint, assertIsEntrypoint, type RefetchQueryNormalizationArtifact, type RefetchQueryNormalizationArtifactWrapper, type ExtractProps, type ExtractReadFromStore, type ExtractResolverResult, } from './core/entrypoint';
|
8
8
|
export { readButDoNotEvaluate } from './core/read';
|
9
9
|
export { type ExtractSecondParam, type Argument, type ArgumentName, type ArgumentValue, type Arguments, } from './core/util';
|
10
|
-
export { type FragmentReference, type Variables, } from './core/FragmentReference';
|
10
|
+
export { type FragmentReference, type Variables, stableIdForFragmentReference, } from './core/FragmentReference';
|
11
11
|
export { IsographEnvironmentProvider, useIsographEnvironment, type IsographEnvironmentProviderProps, } from './react/IsographEnvironmentProvider';
|
12
12
|
export { useImperativeReference } from './react/useImperativeReference';
|
13
13
|
export { FragmentReader } from './react/FragmentReader';
|
14
14
|
export { useResult } from './react/useResult';
|
15
|
+
export { useReadAndSubscribe, useSubscribeToMultiple, } from './react/useReadAndSubscribe';
|
15
16
|
export { useLazyReference } from './react/useLazyReference';
|
16
17
|
export { useRerenderOnChange } from './react/useRerenderOnChange';
|
17
18
|
export { useClientSideDefer } from './loadable-hooks/useClientSideDefer';
|
19
|
+
export { useSuspensefulSkipLimitPagination } from './loadable-hooks/useSuspensefulSkipLimitPagination';
|
package/dist/index.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.useClientSideDefer = exports.useRerenderOnChange = exports.useLazyReference = exports.useResult = exports.FragmentReader = exports.useImperativeReference = exports.useIsographEnvironment = exports.IsographEnvironmentProvider = exports.readButDoNotEvaluate = exports.assertIsEntrypoint = exports.defaultMissingFieldHandler = exports.createIsographStore = exports.createIsographEnvironment = exports.ROOT_ID = exports.makeNetworkRequest = exports.normalizeData = exports.subscribe = exports.getPromiseState = exports.readPromise = exports.garbageCollectEnvironment = exports.unretainQuery = exports.retainQuery = void 0;
|
3
|
+
exports.useSuspensefulSkipLimitPagination = exports.useClientSideDefer = exports.useRerenderOnChange = exports.useLazyReference = exports.useSubscribeToMultiple = exports.useReadAndSubscribe = exports.useResult = exports.FragmentReader = exports.useImperativeReference = exports.useIsographEnvironment = exports.IsographEnvironmentProvider = exports.stableIdForFragmentReference = exports.readButDoNotEvaluate = exports.assertIsEntrypoint = exports.defaultMissingFieldHandler = exports.createIsographStore = exports.createIsographEnvironment = exports.ROOT_ID = exports.makeNetworkRequest = exports.normalizeData = exports.subscribe = exports.getPromiseState = exports.readPromise = exports.garbageCollectEnvironment = exports.unretainQuery = exports.retainQuery = void 0;
|
4
4
|
var garbageCollection_1 = require("./core/garbageCollection");
|
5
5
|
Object.defineProperty(exports, "retainQuery", { enumerable: true, get: function () { return garbageCollection_1.retainQuery; } });
|
6
6
|
Object.defineProperty(exports, "unretainQuery", { enumerable: true, get: function () { return garbageCollection_1.unretainQuery; } });
|
@@ -22,6 +22,8 @@ var entrypoint_1 = require("./core/entrypoint");
|
|
22
22
|
Object.defineProperty(exports, "assertIsEntrypoint", { enumerable: true, get: function () { return entrypoint_1.assertIsEntrypoint; } });
|
23
23
|
var read_1 = require("./core/read");
|
24
24
|
Object.defineProperty(exports, "readButDoNotEvaluate", { enumerable: true, get: function () { return read_1.readButDoNotEvaluate; } });
|
25
|
+
var FragmentReference_1 = require("./core/FragmentReference");
|
26
|
+
Object.defineProperty(exports, "stableIdForFragmentReference", { enumerable: true, get: function () { return FragmentReference_1.stableIdForFragmentReference; } });
|
25
27
|
var IsographEnvironmentProvider_1 = require("./react/IsographEnvironmentProvider");
|
26
28
|
Object.defineProperty(exports, "IsographEnvironmentProvider", { enumerable: true, get: function () { return IsographEnvironmentProvider_1.IsographEnvironmentProvider; } });
|
27
29
|
Object.defineProperty(exports, "useIsographEnvironment", { enumerable: true, get: function () { return IsographEnvironmentProvider_1.useIsographEnvironment; } });
|
@@ -31,9 +33,14 @@ var FragmentReader_1 = require("./react/FragmentReader");
|
|
31
33
|
Object.defineProperty(exports, "FragmentReader", { enumerable: true, get: function () { return FragmentReader_1.FragmentReader; } });
|
32
34
|
var useResult_1 = require("./react/useResult");
|
33
35
|
Object.defineProperty(exports, "useResult", { enumerable: true, get: function () { return useResult_1.useResult; } });
|
36
|
+
var useReadAndSubscribe_1 = require("./react/useReadAndSubscribe");
|
37
|
+
Object.defineProperty(exports, "useReadAndSubscribe", { enumerable: true, get: function () { return useReadAndSubscribe_1.useReadAndSubscribe; } });
|
38
|
+
Object.defineProperty(exports, "useSubscribeToMultiple", { enumerable: true, get: function () { return useReadAndSubscribe_1.useSubscribeToMultiple; } });
|
34
39
|
var useLazyReference_1 = require("./react/useLazyReference");
|
35
40
|
Object.defineProperty(exports, "useLazyReference", { enumerable: true, get: function () { return useLazyReference_1.useLazyReference; } });
|
36
41
|
var useRerenderOnChange_1 = require("./react/useRerenderOnChange");
|
37
42
|
Object.defineProperty(exports, "useRerenderOnChange", { enumerable: true, get: function () { return useRerenderOnChange_1.useRerenderOnChange; } });
|
38
43
|
var useClientSideDefer_1 = require("./loadable-hooks/useClientSideDefer");
|
39
44
|
Object.defineProperty(exports, "useClientSideDefer", { enumerable: true, get: function () { return useClientSideDefer_1.useClientSideDefer; } });
|
45
|
+
var useSuspensefulSkipLimitPagination_1 = require("./loadable-hooks/useSuspensefulSkipLimitPagination");
|
46
|
+
Object.defineProperty(exports, "useSuspensefulSkipLimitPagination", { enumerable: true, get: function () { return useSuspensefulSkipLimitPagination_1.useSuspensefulSkipLimitPagination; } });
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { LoadableField } from '../core/reader';
|
2
|
+
type SkipOrLimit = 'skip' | 'limit';
|
3
|
+
type OmitSkipLimit<TArgs> = keyof Omit<TArgs, SkipOrLimit> extends never ? void | Record<string, never> : Omit<TArgs, SkipOrLimit>;
|
4
|
+
type UseSkipLimitReturnValue<TArgs, TItem> = {
|
5
|
+
readonly fetchMore: (args: OmitSkipLimit<TArgs>, count: number) => void;
|
6
|
+
readonly results: ReadonlyArray<TItem>;
|
7
|
+
};
|
8
|
+
/**
|
9
|
+
* accepts a loadableField that accepts skip and limit arguments
|
10
|
+
* and returns:
|
11
|
+
* - a fetchMore function that, when called, triggers a network
|
12
|
+
* request for additional data, and
|
13
|
+
* - the data received so far.
|
14
|
+
*
|
15
|
+
* This hook will suspend if any network request is in flight.
|
16
|
+
*
|
17
|
+
* Calling fetchMore before the hook mounts is a no-op.
|
18
|
+
*/
|
19
|
+
export declare function useSuspensefulSkipLimitPagination<TArgs extends {
|
20
|
+
skip: number | void | null;
|
21
|
+
limit: number | void | null;
|
22
|
+
}, TItem>(loadableField: LoadableField<TArgs, Array<TItem>>): UseSkipLimitReturnValue<TArgs, TItem>;
|
23
|
+
export {};
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useSuspensefulSkipLimitPagination = 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
|
+
function flatten(arr) {
|
10
|
+
let outArray = [];
|
11
|
+
for (const subarr of arr) {
|
12
|
+
for (const item of subarr) {
|
13
|
+
outArray.push(item);
|
14
|
+
}
|
15
|
+
}
|
16
|
+
return outArray;
|
17
|
+
}
|
18
|
+
/**
|
19
|
+
* accepts a loadableField that accepts skip and limit arguments
|
20
|
+
* and returns:
|
21
|
+
* - a fetchMore function that, when called, triggers a network
|
22
|
+
* request for additional data, and
|
23
|
+
* - the data received so far.
|
24
|
+
*
|
25
|
+
* This hook will suspend if any network request is in flight.
|
26
|
+
*
|
27
|
+
* Calling fetchMore before the hook mounts is a no-op.
|
28
|
+
*/
|
29
|
+
function useSuspensefulSkipLimitPagination(loadableField) {
|
30
|
+
const networkRequestOptions = {
|
31
|
+
suspendIfInFlight: true,
|
32
|
+
throwOnNetworkError: true,
|
33
|
+
};
|
34
|
+
const { state, setState } = (0, react_disposable_state_1.useUpdatableDisposableState)();
|
35
|
+
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
36
|
+
const loadedReferences = state === react_disposable_state_1.UNASSIGNED_STATE ? [] : state;
|
37
|
+
const results = loadedReferences.map(([pointer]) => {
|
38
|
+
const fragmentReference = pointer.getItemIfNotDisposed();
|
39
|
+
if (fragmentReference == null) {
|
40
|
+
throw new Error('FragmentReference is unexpectedly disposed. \
|
41
|
+
This is indicative of a bug in Isograph.');
|
42
|
+
}
|
43
|
+
(0, useResult_1.maybeUnwrapNetworkRequest)(fragmentReference.networkRequest, networkRequestOptions);
|
44
|
+
const data = (0, read_1.readButDoNotEvaluate)(environment, fragmentReference, networkRequestOptions);
|
45
|
+
return fragmentReference.readerArtifact.resolver(data.item, undefined);
|
46
|
+
});
|
47
|
+
const items = flatten(results);
|
48
|
+
const loadedSoFar = items.length;
|
49
|
+
return {
|
50
|
+
fetchMore: (args, count) => {
|
51
|
+
// @ts-expect-error
|
52
|
+
const loadedField = loadableField(Object.assign(Object.assign({}, args), { skip: loadedSoFar, limit: count }))[1]();
|
53
|
+
const newPointer = (0, reference_counted_pointer_1.createReferenceCountedPointer)(loadedField);
|
54
|
+
const clonedPointers = [
|
55
|
+
...loadedReferences.map(([refCountedPointer]) => {
|
56
|
+
const clonedRefCountedPointer = refCountedPointer.cloneIfNotDisposed();
|
57
|
+
if (clonedRefCountedPointer == null) {
|
58
|
+
throw new Error('This reference counted pointer has already been disposed. \
|
59
|
+
This is indicative of a bug in useSuspensefulSkipLimitPagination.');
|
60
|
+
}
|
61
|
+
return clonedRefCountedPointer;
|
62
|
+
}),
|
63
|
+
];
|
64
|
+
const allPointers = [...clonedPointers, newPointer];
|
65
|
+
const totalItemCleanupPair = [
|
66
|
+
allPointers,
|
67
|
+
() => {
|
68
|
+
allPointers.forEach(([, dispose]) => {
|
69
|
+
dispose();
|
70
|
+
});
|
71
|
+
},
|
72
|
+
];
|
73
|
+
setState(totalItemCleanupPair);
|
74
|
+
},
|
75
|
+
results: items,
|
76
|
+
};
|
77
|
+
}
|
78
|
+
exports.useSuspensefulSkipLimitPagination = useSuspensefulSkipLimitPagination;
|
@@ -1,7 +1,11 @@
|
|
1
1
|
import { FragmentReference } from '../core/FragmentReference';
|
2
|
-
import { NetworkRequestReaderOptions } from '../core/read';
|
2
|
+
import { NetworkRequestReaderOptions, WithEncounteredRecords } from '../core/read';
|
3
3
|
/**
|
4
4
|
* Read the data from a fragment reference and subscribe to updates.
|
5
|
-
* Does not pass the data to the fragment reference's resolver function.
|
6
5
|
*/
|
7
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;
|
@@ -1,13 +1,14 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.useReadAndSubscribe = void 0;
|
3
|
+
exports.useSubscribeToMultiple = exports.useReadAndSubscribe = void 0;
|
4
4
|
const react_1 = require("react");
|
5
|
+
const FragmentReference_1 = require("../core/FragmentReference");
|
5
6
|
const read_1 = require("../core/read");
|
6
7
|
const useRerenderOnChange_1 = require("./useRerenderOnChange");
|
7
8
|
const IsographEnvironmentProvider_1 = require("./IsographEnvironmentProvider");
|
9
|
+
const cache_1 = require("../core/cache");
|
8
10
|
/**
|
9
11
|
* Read the data from a fragment reference and subscribe to updates.
|
10
|
-
* Does not pass the data to the fragment reference's resolver function.
|
11
12
|
*/
|
12
13
|
function useReadAndSubscribe(fragmentReference, networkRequestOptions) {
|
13
14
|
const environment = (0, IsographEnvironmentProvider_1.useIsographEnvironment)();
|
@@ -16,3 +17,25 @@ function useReadAndSubscribe(fragmentReference, networkRequestOptions) {
|
|
16
17
|
return readOutDataAndRecords.item;
|
17
18
|
}
|
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;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@isograph/react",
|
3
|
-
"version": "0.0.0-main-
|
3
|
+
"version": "0.0.0-main-6c4d9bd8",
|
4
4
|
"description": "Use Isograph with React",
|
5
5
|
"homepage": "https://isograph.dev",
|
6
6
|
"main": "dist/index.js",
|
@@ -17,8 +17,9 @@
|
|
17
17
|
"tsc": "tsc"
|
18
18
|
},
|
19
19
|
"dependencies": {
|
20
|
-
"@isograph/disposable-types": "0.0.0-main-
|
21
|
-
"@isograph/react-disposable-state": "0.0.0-main-
|
20
|
+
"@isograph/disposable-types": "0.0.0-main-6c4d9bd8",
|
21
|
+
"@isograph/react-disposable-state": "0.0.0-main-6c4d9bd8",
|
22
|
+
"@isograph/reference-counted-pointer": "0.0.0-main-6c4d9bd8",
|
22
23
|
"react": "^18.2.0"
|
23
24
|
},
|
24
25
|
"devDependencies": {
|
@@ -24,3 +24,19 @@ export type FragmentReference<
|
|
24
24
|
readonly nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[];
|
25
25
|
readonly networkRequest: PromiseWrapper<void, any>;
|
26
26
|
};
|
27
|
+
|
28
|
+
export function stableIdForFragmentReference(
|
29
|
+
fragmentReference: FragmentReference<any, any>,
|
30
|
+
): string {
|
31
|
+
return `${fragmentReference.root}/TODO_FRAGMENT_NAME/${serializeVariables(fragmentReference.variables ?? {})}`;
|
32
|
+
}
|
33
|
+
|
34
|
+
function serializeVariables(variables: Variables) {
|
35
|
+
let s = '';
|
36
|
+
const keys = Object.keys(variables);
|
37
|
+
keys.sort();
|
38
|
+
for (const key of keys) {
|
39
|
+
s += `${key}:${variables[key]},`;
|
40
|
+
}
|
41
|
+
return s;
|
42
|
+
}
|
@@ -11,7 +11,7 @@ type ComponentCache = {
|
|
11
11
|
};
|
12
12
|
};
|
13
13
|
|
14
|
-
type FragmentSubscription<TReadFromStore extends Object> = {
|
14
|
+
export type FragmentSubscription<TReadFromStore extends Object> = {
|
15
15
|
readonly kind: 'FragmentSubscription';
|
16
16
|
readonly callback: (
|
17
17
|
newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
|
package/src/core/cache.ts
CHANGED
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
type IsographEnvironment,
|
9
9
|
DataTypeValue,
|
10
10
|
getLink,
|
11
|
+
FragmentSubscription,
|
11
12
|
} from './IsographEnvironment';
|
12
13
|
import {
|
13
14
|
IsographEntrypoint,
|
@@ -131,6 +132,7 @@ export function normalizeData(
|
|
131
132
|
console.log('after normalization', {
|
132
133
|
store: environment.store,
|
133
134
|
encounteredIds,
|
135
|
+
environment,
|
134
136
|
});
|
135
137
|
}
|
136
138
|
callSubscriptions(environment, encounteredIds);
|
@@ -149,6 +151,7 @@ export function subscribeToAnyChange(
|
|
149
151
|
return () => environment.subscriptions.delete(subscription);
|
150
152
|
}
|
151
153
|
|
154
|
+
// TODO we should re-read and call callback if the value has changed
|
152
155
|
export function subscribe<TReadFromStore extends Object>(
|
153
156
|
environment: IsographEnvironment,
|
154
157
|
encounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
|
@@ -157,12 +160,12 @@ export function subscribe<TReadFromStore extends Object>(
|
|
157
160
|
newEncounteredDataAndRecords: WithEncounteredRecords<TReadFromStore>,
|
158
161
|
) => void,
|
159
162
|
): () => void {
|
160
|
-
const fragmentSubscription = {
|
163
|
+
const fragmentSubscription: FragmentSubscription<TReadFromStore> = {
|
161
164
|
kind: 'FragmentSubscription',
|
162
165
|
callback,
|
163
166
|
encounteredDataAndRecords,
|
164
167
|
fragmentReference,
|
165
|
-
}
|
168
|
+
};
|
166
169
|
// @ts-expect-error
|
167
170
|
environment.subscriptions.add(fragmentSubscription);
|
168
171
|
// @ts-expect-error
|
package/src/index.ts
CHANGED
@@ -60,6 +60,7 @@ export {
|
|
60
60
|
export {
|
61
61
|
type FragmentReference,
|
62
62
|
type Variables,
|
63
|
+
stableIdForFragmentReference,
|
63
64
|
} from './core/FragmentReference';
|
64
65
|
|
65
66
|
export {
|
@@ -70,7 +71,12 @@ export {
|
|
70
71
|
export { useImperativeReference } from './react/useImperativeReference';
|
71
72
|
export { FragmentReader } from './react/FragmentReader';
|
72
73
|
export { useResult } from './react/useResult';
|
74
|
+
export {
|
75
|
+
useReadAndSubscribe,
|
76
|
+
useSubscribeToMultiple,
|
77
|
+
} from './react/useReadAndSubscribe';
|
73
78
|
export { useLazyReference } from './react/useLazyReference';
|
74
79
|
export { useRerenderOnChange } from './react/useRerenderOnChange';
|
75
80
|
|
76
81
|
export { useClientSideDefer } from './loadable-hooks/useClientSideDefer';
|
82
|
+
export { useSuspensefulSkipLimitPagination } from './loadable-hooks/useSuspensefulSkipLimitPagination';
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import { LoadableField } from '../core/reader';
|
2
|
+
import { useIsographEnvironment } from '../react/IsographEnvironmentProvider';
|
3
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
4
|
+
import { FragmentReference } from '../core/FragmentReference';
|
5
|
+
import { maybeUnwrapNetworkRequest } from '../react/useResult';
|
6
|
+
import { readButDoNotEvaluate } from '../core/read';
|
7
|
+
import {
|
8
|
+
UNASSIGNED_STATE,
|
9
|
+
useUpdatableDisposableState,
|
10
|
+
} from '@isograph/react-disposable-state';
|
11
|
+
import {
|
12
|
+
createReferenceCountedPointer,
|
13
|
+
ReferenceCountedPointer,
|
14
|
+
} from '@isograph/reference-counted-pointer';
|
15
|
+
|
16
|
+
type SkipOrLimit = 'skip' | 'limit';
|
17
|
+
type OmitSkipLimit<TArgs> = keyof Omit<TArgs, SkipOrLimit> extends never
|
18
|
+
? void | Record<string, never>
|
19
|
+
: Omit<TArgs, SkipOrLimit>;
|
20
|
+
|
21
|
+
type UseSkipLimitReturnValue<TArgs, TItem> = {
|
22
|
+
readonly fetchMore: (args: OmitSkipLimit<TArgs>, count: number) => void;
|
23
|
+
readonly results: ReadonlyArray<TItem>;
|
24
|
+
};
|
25
|
+
|
26
|
+
type ArrayFragmentReference<TItem> = FragmentReference<
|
27
|
+
any,
|
28
|
+
ReadonlyArray<TItem>
|
29
|
+
>;
|
30
|
+
|
31
|
+
type LoadedFragmentReferences<TItem> = ReadonlyArray<
|
32
|
+
ItemCleanupPair<ReferenceCountedPointer<ArrayFragmentReference<TItem>>>
|
33
|
+
>;
|
34
|
+
|
35
|
+
function flatten<T>(arr: ReadonlyArray<ReadonlyArray<T>>): ReadonlyArray<T> {
|
36
|
+
let outArray: Array<T> = [];
|
37
|
+
for (const subarr of arr) {
|
38
|
+
for (const item of subarr) {
|
39
|
+
outArray.push(item);
|
40
|
+
}
|
41
|
+
}
|
42
|
+
return outArray;
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* accepts a loadableField that accepts skip and limit arguments
|
47
|
+
* and returns:
|
48
|
+
* - a fetchMore function that, when called, triggers a network
|
49
|
+
* request for additional data, and
|
50
|
+
* - the data received so far.
|
51
|
+
*
|
52
|
+
* This hook will suspend if any network request is in flight.
|
53
|
+
*
|
54
|
+
* Calling fetchMore before the hook mounts is a no-op.
|
55
|
+
*/
|
56
|
+
export function useSuspensefulSkipLimitPagination<
|
57
|
+
TArgs extends {
|
58
|
+
skip: number | void | null;
|
59
|
+
limit: number | void | null;
|
60
|
+
},
|
61
|
+
TItem,
|
62
|
+
>(
|
63
|
+
loadableField: LoadableField<TArgs, Array<TItem>>,
|
64
|
+
): UseSkipLimitReturnValue<TArgs, TItem> {
|
65
|
+
const networkRequestOptions = {
|
66
|
+
suspendIfInFlight: true,
|
67
|
+
throwOnNetworkError: true,
|
68
|
+
};
|
69
|
+
const { state, setState } =
|
70
|
+
useUpdatableDisposableState<LoadedFragmentReferences<TItem>>();
|
71
|
+
|
72
|
+
const environment = useIsographEnvironment();
|
73
|
+
|
74
|
+
const loadedReferences = state === UNASSIGNED_STATE ? [] : state;
|
75
|
+
|
76
|
+
const results = loadedReferences.map(([pointer]) => {
|
77
|
+
const fragmentReference = pointer.getItemIfNotDisposed();
|
78
|
+
if (fragmentReference == null) {
|
79
|
+
throw new Error(
|
80
|
+
'FragmentReference is unexpectedly disposed. \
|
81
|
+
This is indicative of a bug in Isograph.',
|
82
|
+
);
|
83
|
+
}
|
84
|
+
|
85
|
+
maybeUnwrapNetworkRequest(
|
86
|
+
fragmentReference.networkRequest,
|
87
|
+
networkRequestOptions,
|
88
|
+
);
|
89
|
+
const data = readButDoNotEvaluate(
|
90
|
+
environment,
|
91
|
+
fragmentReference,
|
92
|
+
networkRequestOptions,
|
93
|
+
);
|
94
|
+
return fragmentReference.readerArtifact.resolver(
|
95
|
+
data.item,
|
96
|
+
undefined,
|
97
|
+
) as ReadonlyArray<any>;
|
98
|
+
});
|
99
|
+
|
100
|
+
const items = flatten(results);
|
101
|
+
const loadedSoFar = items.length;
|
102
|
+
|
103
|
+
return {
|
104
|
+
fetchMore: (args, count) => {
|
105
|
+
// @ts-expect-error
|
106
|
+
const loadedField = loadableField({
|
107
|
+
...args,
|
108
|
+
skip: loadedSoFar,
|
109
|
+
limit: count,
|
110
|
+
})[1]();
|
111
|
+
const newPointer = createReferenceCountedPointer(loadedField);
|
112
|
+
const clonedPointers = [
|
113
|
+
...loadedReferences.map(([refCountedPointer]) => {
|
114
|
+
const clonedRefCountedPointer =
|
115
|
+
refCountedPointer.cloneIfNotDisposed();
|
116
|
+
if (clonedRefCountedPointer == null) {
|
117
|
+
throw new Error(
|
118
|
+
'This reference counted pointer has already been disposed. \
|
119
|
+
This is indicative of a bug in useSuspensefulSkipLimitPagination.',
|
120
|
+
);
|
121
|
+
}
|
122
|
+
return clonedRefCountedPointer;
|
123
|
+
}),
|
124
|
+
];
|
125
|
+
|
126
|
+
const allPointers = [...clonedPointers, newPointer];
|
127
|
+
|
128
|
+
const totalItemCleanupPair: ItemCleanupPair<
|
129
|
+
ReadonlyArray<
|
130
|
+
ItemCleanupPair<
|
131
|
+
ReferenceCountedPointer<ArrayFragmentReference<TItem>>
|
132
|
+
>
|
133
|
+
>
|
134
|
+
> = [
|
135
|
+
allPointers,
|
136
|
+
() => {
|
137
|
+
allPointers.forEach(([, dispose]) => {
|
138
|
+
dispose();
|
139
|
+
});
|
140
|
+
},
|
141
|
+
];
|
142
|
+
|
143
|
+
setState(totalItemCleanupPair);
|
144
|
+
},
|
145
|
+
results: items,
|
146
|
+
};
|
147
|
+
}
|
@@ -1,15 +1,19 @@
|
|
1
|
-
import { useState } from 'react';
|
2
|
-
import {
|
1
|
+
import { useEffect, useState } from 'react';
|
2
|
+
import {
|
3
|
+
FragmentReference,
|
4
|
+
stableIdForFragmentReference,
|
5
|
+
} from '../core/FragmentReference';
|
3
6
|
import {
|
4
7
|
NetworkRequestReaderOptions,
|
5
8
|
readButDoNotEvaluate,
|
9
|
+
WithEncounteredRecords,
|
6
10
|
} from '../core/read';
|
7
11
|
import { useRerenderOnChange } from './useRerenderOnChange';
|
8
12
|
import { useIsographEnvironment } from './IsographEnvironmentProvider';
|
13
|
+
import { subscribe } from '../core/cache';
|
9
14
|
|
10
15
|
/**
|
11
16
|
* Read the data from a fragment reference and subscribe to updates.
|
12
|
-
* Does not pass the data to the fragment reference's resolver function.
|
13
17
|
*/
|
14
18
|
export function useReadAndSubscribe<TReadFromStore extends Object>(
|
15
19
|
fragmentReference: FragmentReference<TReadFromStore, any>,
|
@@ -26,3 +30,37 @@ export function useReadAndSubscribe<TReadFromStore extends Object>(
|
|
26
30
|
);
|
27
31
|
return readOutDataAndRecords.item;
|
28
32
|
}
|
33
|
+
|
34
|
+
export function useSubscribeToMultiple<TReadFromStore extends Object>(
|
35
|
+
items: ReadonlyArray<{
|
36
|
+
records: WithEncounteredRecords<TReadFromStore>;
|
37
|
+
callback: (updatedRecords: WithEncounteredRecords<TReadFromStore>) => void;
|
38
|
+
fragmentReference: FragmentReference<TReadFromStore, any>;
|
39
|
+
}>,
|
40
|
+
) {
|
41
|
+
const environment = useIsographEnvironment();
|
42
|
+
useEffect(
|
43
|
+
() => {
|
44
|
+
const cleanupFns = items.map(
|
45
|
+
({ records, callback, fragmentReference }) => {
|
46
|
+
return subscribe(environment, records, fragmentReference, callback);
|
47
|
+
},
|
48
|
+
);
|
49
|
+
return () => {
|
50
|
+
cleanupFns.forEach((loader) => {
|
51
|
+
loader();
|
52
|
+
});
|
53
|
+
};
|
54
|
+
},
|
55
|
+
// By analogy to useReadAndSubscribe, we can have an empty dependency array?
|
56
|
+
// Maybe callback has to be depended on. I don't know!
|
57
|
+
// TODO find out
|
58
|
+
[
|
59
|
+
items
|
60
|
+
.map(({ fragmentReference }) =>
|
61
|
+
stableIdForFragmentReference(fragmentReference),
|
62
|
+
)
|
63
|
+
.join('.'),
|
64
|
+
],
|
65
|
+
);
|
66
|
+
}
|