@isograph/react 0.0.0-main-945e49cc → 0.0.0-main-dc9ca2f6

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.
@@ -1,6 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
2
  import * as React from 'react';
3
3
  import { ParentCache } from '@isograph/isograph-react-disposable-state';
4
+ import { NormalizationAst } from './index';
4
5
  export declare const IsographEnvironmentContext: React.Context<IsographEnvironment | null>;
5
6
  type ComponentName = string;
6
7
  type StringifiedArgs = string;
@@ -15,6 +16,10 @@ export type Subscriptions = Set<() => void>;
15
16
  type SuspenseCache = {
16
17
  [index: string]: ParentCache<any>;
17
18
  };
19
+ export type RetainedQuery = {
20
+ normalizationAst: NormalizationAst;
21
+ variables: {};
22
+ };
18
23
  export type IsographEnvironment = {
19
24
  store: IsographStore;
20
25
  networkFunction: IsographNetworkFunction;
@@ -22,6 +27,9 @@ export type IsographEnvironment = {
22
27
  componentCache: ComponentCache;
23
28
  subscriptions: Subscriptions;
24
29
  suspenseCache: SuspenseCache;
30
+ retainedQueries: Set<RetainedQuery>;
31
+ gcBuffer: Array<RetainedQuery>;
32
+ gcBufferSize: number;
25
33
  };
26
34
  export type MissingFieldHandler = (storeRecord: StoreRecord, root: DataId, fieldName: string, arguments_: {
27
35
  [index: string]: any;
@@ -53,4 +61,8 @@ export declare function createIsographEnvironment(store: IsographStore, networkF
53
61
  export declare function createIsographStore(): {
54
62
  __ROOT: {};
55
63
  };
64
+ export declare function garbageCollectEnvironment(environment: IsographEnvironment): void;
65
+ type DidUnretainSomeQuery = boolean;
66
+ export declare function unretainQuery(environment: IsographEnvironment, retainedQuery: RetainedQuery): DidUnretainSomeQuery;
67
+ export declare function retainQuery(environment: IsographEnvironment, queryToRetain: RetainedQuery): void;
56
68
  export {};
@@ -23,9 +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.createIsographStore = exports.createIsographEnvironment = exports.useIsographEnvironment = exports.IsographEnvironmentProvider = exports.ROOT_ID = exports.IsographEnvironmentContext = void 0;
26
+ exports.retainQuery = exports.unretainQuery = exports.garbageCollectEnvironment = exports.createIsographStore = exports.createIsographEnvironment = exports.useIsographEnvironment = exports.IsographEnvironmentProvider = exports.ROOT_ID = exports.IsographEnvironmentContext = void 0;
27
27
  const react_1 = require("react");
28
28
  const React = __importStar(require("react"));
29
+ const cache_1 = require("./cache");
29
30
  exports.IsographEnvironmentContext = (0, react_1.createContext)(null);
30
31
  exports.ROOT_ID = '__ROOT';
31
32
  function IsographEnvironmentProvider({ environment, children, }) {
@@ -41,6 +42,7 @@ function useIsographEnvironment() {
41
42
  return context;
42
43
  }
43
44
  exports.useIsographEnvironment = useIsographEnvironment;
45
+ const DEFAULT_GC_BUFFER_SIZE = 10;
44
46
  function createIsographEnvironment(store, networkFunction, missingFieldHandler) {
45
47
  return {
46
48
  store,
@@ -49,6 +51,9 @@ function createIsographEnvironment(store, networkFunction, missingFieldHandler)
49
51
  componentCache: {},
50
52
  subscriptions: new Set(),
51
53
  suspenseCache: {},
54
+ retainedQueries: new Set(),
55
+ gcBuffer: [],
56
+ gcBufferSize: DEFAULT_GC_BUFFER_SIZE,
52
57
  };
53
58
  }
54
59
  exports.createIsographEnvironment = createIsographEnvironment;
@@ -58,3 +63,82 @@ function createIsographStore() {
58
63
  };
59
64
  }
60
65
  exports.createIsographStore = createIsographStore;
66
+ function garbageCollectEnvironment(environment) {
67
+ const retainedIds = new Set([exports.ROOT_ID]);
68
+ for (const query of environment.retainedQueries) {
69
+ recordReachableIds(environment.store, query, retainedIds);
70
+ }
71
+ for (const query of environment.gcBuffer) {
72
+ recordReachableIds(environment.store, query, retainedIds);
73
+ }
74
+ for (const dataId in environment.store) {
75
+ if (!retainedIds.has(dataId)) {
76
+ delete environment.store[dataId];
77
+ }
78
+ }
79
+ }
80
+ exports.garbageCollectEnvironment = garbageCollectEnvironment;
81
+ function recordReachableIds(store, retainedQuery, mutableRetainedIds) {
82
+ recordReachableIdsFromRecord(store, store[exports.ROOT_ID], mutableRetainedIds, retainedQuery.normalizationAst, retainedQuery.variables);
83
+ }
84
+ function getLinkedId(data) {
85
+ // @ts-expect-error
86
+ if (data.__link != null) {
87
+ // @ts-expect-error
88
+ return data.__link;
89
+ }
90
+ else {
91
+ throw new Error('Record in an invalid state');
92
+ }
93
+ }
94
+ function recordReachableIdsFromRecord(store, currentRecord, mutableRetainedIds, selections, variables) {
95
+ for (const selection of selections) {
96
+ switch (selection.kind) {
97
+ case 'Linked':
98
+ const linkKey = (0, cache_1.getParentRecordKey)(selection, variables !== null && variables !== void 0 ? variables : {});
99
+ const linkedFieldOrFields = currentRecord[linkKey];
100
+ const ids = [];
101
+ if (Array.isArray(linkedFieldOrFields)) {
102
+ for (const link of linkedFieldOrFields) {
103
+ if (link != null) {
104
+ const id = getLinkedId(link);
105
+ ids.push(id);
106
+ }
107
+ }
108
+ }
109
+ else {
110
+ if (linkedFieldOrFields != null) {
111
+ const id = getLinkedId(linkedFieldOrFields);
112
+ ids.push(id);
113
+ }
114
+ }
115
+ for (const nextRecordId of ids) {
116
+ const nextRecord = store[nextRecordId];
117
+ if (nextRecord != null) {
118
+ mutableRetainedIds.add(nextRecordId);
119
+ recordReachableIdsFromRecord(store, nextRecord, mutableRetainedIds, selection.selections, variables);
120
+ }
121
+ }
122
+ continue;
123
+ case 'Scalar':
124
+ continue;
125
+ }
126
+ }
127
+ }
128
+ function unretainQuery(environment, retainedQuery) {
129
+ environment.retainedQueries.delete(retainedQuery);
130
+ environment.gcBuffer.push(retainedQuery);
131
+ if (environment.gcBuffer.length > environment.gcBufferSize) {
132
+ environment.gcBuffer.shift();
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+ exports.unretainQuery = unretainQuery;
138
+ function retainQuery(environment, queryToRetain) {
139
+ environment.retainedQueries.add(queryToRetain);
140
+ // TODO can we remove this query from the buffer somehow?
141
+ // We are relying on === equality, but we really should be comparing
142
+ // id + variables
143
+ }
144
+ exports.retainQuery = retainQuery;
package/dist/cache.js CHANGED
@@ -49,34 +49,60 @@ function makeNetworkRequest(environment, artifact, variables) {
49
49
  if (typeof window !== 'undefined' && window.__LOG) {
50
50
  console.log('make network request', artifact, variables);
51
51
  }
52
+ let status = {
53
+ kind: 'UndisposedIncomplete',
54
+ };
55
+ // This should be an observable, not a promise
52
56
  const promise = environment
53
57
  .networkFunction(artifact.queryText, variables)
54
58
  .then((networkResponse) => {
55
59
  if (typeof window !== 'undefined' && window.__LOG) {
56
60
  console.log('network response', artifact, artifact);
57
61
  }
58
- normalizeData(environment, artifact.normalizationAst, networkResponse.data, variables, artifact.nestedRefetchQueries);
59
- return networkResponse.data;
62
+ if (status.kind === 'UndisposedIncomplete') {
63
+ normalizeData(environment, artifact.normalizationAst, networkResponse.data, variables, artifact.nestedRefetchQueries);
64
+ const retainedQuery = {
65
+ normalizationAst: artifact.normalizationAst,
66
+ variables,
67
+ };
68
+ status = {
69
+ kind: 'UndisposedComplete',
70
+ retainedQuery,
71
+ };
72
+ (0, IsographEnvironment_1.retainQuery)(environment, retainedQuery);
73
+ }
74
+ // TODO return null
75
+ return networkResponse;
60
76
  });
61
77
  const wrapper = (0, PromiseWrapper_1.wrapPromise)(promise);
62
78
  const response = [
63
79
  wrapper,
64
80
  () => {
65
- // delete from cache
81
+ if (status.kind === 'UndisposedComplete') {
82
+ const didUnretainSomeQuery = (0, IsographEnvironment_1.unretainQuery)(environment, status.retainedQuery);
83
+ if (didUnretainSomeQuery) {
84
+ (0, IsographEnvironment_1.garbageCollectEnvironment)(environment);
85
+ }
86
+ }
87
+ status = {
88
+ kind: 'Disposed',
89
+ };
66
90
  },
67
91
  ];
68
92
  return response;
69
93
  }
70
94
  exports.makeNetworkRequest = makeNetworkRequest;
71
95
  function normalizeData(environment, normalizationAst, networkResponse, variables, nestedRefetchQueries) {
96
+ const encounteredIds = new Set();
72
97
  if (typeof window !== 'undefined' && window.__LOG) {
73
98
  console.log('about to normalize', normalizationAst, networkResponse, variables);
74
99
  }
75
- normalizeDataIntoRecord(environment, normalizationAst, networkResponse, environment.store.__ROOT, IsographEnvironment_1.ROOT_ID, variables, nestedRefetchQueries);
100
+ normalizeDataIntoRecord(environment, normalizationAst, networkResponse, environment.store.__ROOT, IsographEnvironment_1.ROOT_ID, variables, nestedRefetchQueries, encounteredIds);
76
101
  if (typeof window !== 'undefined' && window.__LOG) {
77
102
  console.log('after normalization', { store: environment.store });
78
103
  }
79
104
  callSubscriptions(environment);
105
+ return encounteredIds;
80
106
  }
81
107
  function subscribe(environment, callback) {
82
108
  environment.subscriptions.add(callback);
@@ -98,7 +124,8 @@ function callSubscriptions(environment) {
98
124
  /**
99
125
  * Mutate targetParentRecord according to the normalizationAst and networkResponseParentRecord.
100
126
  */
101
- function normalizeDataIntoRecord(environment, normalizationAst, networkResponseParentRecord, targetParentRecord, targetParentRecordId, variables, nestedRefetchQueries) {
127
+ function normalizeDataIntoRecord(environment, normalizationAst, networkResponseParentRecord, targetParentRecord, targetParentRecordId, variables, nestedRefetchQueries, mutableEncounteredIds) {
128
+ mutableEncounteredIds.add(targetParentRecordId);
102
129
  for (const normalizationNode of normalizationAst) {
103
130
  switch (normalizationNode.kind) {
104
131
  case 'Scalar': {
@@ -106,7 +133,7 @@ function normalizeDataIntoRecord(environment, normalizationAst, networkResponseP
106
133
  break;
107
134
  }
108
135
  case 'Linked': {
109
- normalizeLinkedField(environment, normalizationNode, networkResponseParentRecord, targetParentRecord, targetParentRecordId, variables, nestedRefetchQueries);
136
+ normalizeLinkedField(environment, normalizationNode, networkResponseParentRecord, targetParentRecord, targetParentRecordId, variables, nestedRefetchQueries, mutableEncounteredIds);
110
137
  break;
111
138
  }
112
139
  }
@@ -127,7 +154,7 @@ function normalizeScalarField(astNode, networkResponseParentRecord, targetStoreR
127
154
  /**
128
155
  * Mutate targetParentRecord with a given linked field ast node.
129
156
  */
130
- function normalizeLinkedField(environment, astNode, networkResponseParentRecord, targetParentRecord, targetParentRecordId, variables, nestedRefetchQueries) {
157
+ function normalizeLinkedField(environment, astNode, networkResponseParentRecord, targetParentRecord, targetParentRecordId, variables, nestedRefetchQueries, mutableEncounteredIds) {
131
158
  const networkResponseKey = getNetworkResponseKey(astNode);
132
159
  const networkResponseData = networkResponseParentRecord[networkResponseKey];
133
160
  const parentRecordKey = getParentRecordKey(astNode, variables);
@@ -143,24 +170,24 @@ function normalizeLinkedField(environment, astNode, networkResponseParentRecord,
143
170
  const dataIds = [];
144
171
  for (let i = 0; i < networkResponseData.length; i++) {
145
172
  const networkResponseObject = networkResponseData[i];
146
- const newStoreRecordId = normalizeNetworkResponseObject(environment, astNode, networkResponseObject, targetParentRecordId, variables, i, nestedRefetchQueries);
173
+ const newStoreRecordId = normalizeNetworkResponseObject(environment, astNode, networkResponseObject, targetParentRecordId, variables, i, nestedRefetchQueries, mutableEncounteredIds);
147
174
  dataIds.push({ __link: newStoreRecordId });
148
175
  }
149
176
  targetParentRecord[parentRecordKey] = dataIds;
150
177
  }
151
178
  else {
152
- const newStoreRecordId = normalizeNetworkResponseObject(environment, astNode, networkResponseData, targetParentRecordId, variables, null, nestedRefetchQueries);
179
+ const newStoreRecordId = normalizeNetworkResponseObject(environment, astNode, networkResponseData, targetParentRecordId, variables, null, nestedRefetchQueries, mutableEncounteredIds);
153
180
  targetParentRecord[parentRecordKey] = {
154
181
  __link: newStoreRecordId,
155
182
  };
156
183
  }
157
184
  }
158
- function normalizeNetworkResponseObject(environment, astNode, networkResponseData, targetParentRecordId, variables, index, nestedRefetchQueries) {
185
+ function normalizeNetworkResponseObject(environment, astNode, networkResponseData, targetParentRecordId, variables, index, nestedRefetchQueries, mutableEncounteredIds) {
159
186
  var _a;
160
187
  const newStoreRecordId = getDataIdOfNetworkResponse(targetParentRecordId, networkResponseData, astNode, variables, index);
161
188
  const newStoreRecord = (_a = environment.store[newStoreRecordId]) !== null && _a !== void 0 ? _a : {};
162
189
  environment.store[newStoreRecordId] = newStoreRecord;
163
- normalizeDataIntoRecord(environment, astNode.selections, networkResponseData, newStoreRecord, newStoreRecordId, variables, nestedRefetchQueries);
190
+ normalizeDataIntoRecord(environment, astNode.selections, networkResponseData, newStoreRecord, newStoreRecordId, variables, nestedRefetchQueries, mutableEncounteredIds);
164
191
  return newStoreRecordId;
165
192
  }
166
193
  function isScalarOrEmptyArray(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isograph/react",
3
- "version": "0.0.0-main-945e49cc",
3
+ "version": "0.0.0-main-dc9ca2f6",
4
4
  "description": "Use Isograph with React",
5
5
  "homepage": "https://isograph.dev",
6
6
  "main": "dist/index.js",
@@ -16,8 +16,8 @@
16
16
  "prepack": "yarn run test && yarn run compile"
17
17
  },
18
18
  "dependencies": {
19
- "@isograph/disposable-types": "0.0.0-main-945e49cc",
20
- "@isograph/react-disposable-state": "0.0.0-main-945e49cc",
19
+ "@isograph/disposable-types": "0.0.0-main-dc9ca2f6",
20
+ "@isograph/react-disposable-state": "0.0.0-main-dc9ca2f6",
21
21
  "react": "^18.2.0"
22
22
  },
23
23
  "devDependencies": {
@@ -1,6 +1,8 @@
1
1
  import { ReactNode, createContext, useContext } from 'react';
2
2
  import * as React from 'react';
3
3
  import { ParentCache } from '@isograph/isograph-react-disposable-state';
4
+ import { NormalizationAst } from './index';
5
+ import { getParentRecordKey } from './cache';
4
6
 
5
7
  export const IsographEnvironmentContext =
6
8
  createContext<IsographEnvironment | null>(null);
@@ -16,6 +18,11 @@ type ComponentCache = {
16
18
  export type Subscriptions = Set<() => void>;
17
19
  type SuspenseCache = { [index: string]: ParentCache<any> };
18
20
 
21
+ export type RetainedQuery = {
22
+ normalizationAst: NormalizationAst;
23
+ variables: {};
24
+ };
25
+
19
26
  export type IsographEnvironment = {
20
27
  store: IsographStore;
21
28
  networkFunction: IsographNetworkFunction;
@@ -23,6 +30,9 @@ export type IsographEnvironment = {
23
30
  componentCache: ComponentCache;
24
31
  subscriptions: Subscriptions;
25
32
  suspenseCache: SuspenseCache;
33
+ retainedQueries: Set<RetainedQuery>;
34
+ gcBuffer: Array<RetainedQuery>;
35
+ gcBufferSize: number;
26
36
  };
27
37
 
28
38
  export type MissingFieldHandler = (
@@ -98,6 +108,7 @@ export function useIsographEnvironment(): IsographEnvironment {
98
108
  return context;
99
109
  }
100
110
 
111
+ const DEFAULT_GC_BUFFER_SIZE = 10;
101
112
  export function createIsographEnvironment(
102
113
  store: IsographStore,
103
114
  networkFunction: IsographNetworkFunction,
@@ -110,6 +121,9 @@ export function createIsographEnvironment(
110
121
  componentCache: {},
111
122
  subscriptions: new Set(),
112
123
  suspenseCache: {},
124
+ retainedQueries: new Set(),
125
+ gcBuffer: [],
126
+ gcBufferSize: DEFAULT_GC_BUFFER_SIZE,
113
127
  };
114
128
  }
115
129
 
@@ -118,3 +132,119 @@ export function createIsographStore() {
118
132
  [ROOT_ID]: {},
119
133
  };
120
134
  }
135
+
136
+ export function garbageCollectEnvironment(environment: IsographEnvironment) {
137
+ const retainedIds = new Set<DataId>([ROOT_ID]);
138
+
139
+ for (const query of environment.retainedQueries) {
140
+ recordReachableIds(environment.store, query, retainedIds);
141
+ }
142
+ for (const query of environment.gcBuffer) {
143
+ recordReachableIds(environment.store, query, retainedIds);
144
+ }
145
+
146
+ for (const dataId in environment.store) {
147
+ if (!retainedIds.has(dataId)) {
148
+ delete environment.store[dataId];
149
+ }
150
+ }
151
+ }
152
+
153
+ function recordReachableIds(
154
+ store: IsographStore,
155
+ retainedQuery: RetainedQuery,
156
+ mutableRetainedIds: Set<DataId>,
157
+ ) {
158
+ recordReachableIdsFromRecord(
159
+ store,
160
+ store[ROOT_ID],
161
+ mutableRetainedIds,
162
+ retainedQuery.normalizationAst,
163
+ retainedQuery.variables,
164
+ );
165
+ }
166
+
167
+ function getLinkedId(data: Exclude<DataTypeValue, null | void>): string {
168
+ // @ts-expect-error
169
+ if (data.__link != null) {
170
+ // @ts-expect-error
171
+ return data.__link;
172
+ } else {
173
+ throw new Error('Record in an invalid state');
174
+ }
175
+ }
176
+
177
+ function recordReachableIdsFromRecord(
178
+ store: IsographStore,
179
+ currentRecord: StoreRecord,
180
+ mutableRetainedIds: Set<DataId>,
181
+ selections: NormalizationAst,
182
+ variables: { [index: string]: string } | null,
183
+ ) {
184
+ for (const selection of selections) {
185
+ switch (selection.kind) {
186
+ case 'Linked':
187
+ const linkKey = getParentRecordKey(selection, variables ?? {});
188
+ const linkedFieldOrFields = currentRecord[linkKey];
189
+
190
+ const ids = [];
191
+ if (Array.isArray(linkedFieldOrFields)) {
192
+ for (const link of linkedFieldOrFields) {
193
+ if (link != null) {
194
+ const id = getLinkedId(link);
195
+ ids.push(id);
196
+ }
197
+ }
198
+ } else {
199
+ if (linkedFieldOrFields != null) {
200
+ const id = getLinkedId(linkedFieldOrFields);
201
+ ids.push(id);
202
+ }
203
+ }
204
+
205
+ for (const nextRecordId of ids) {
206
+ const nextRecord = store[nextRecordId];
207
+ if (nextRecord != null) {
208
+ mutableRetainedIds.add(nextRecordId);
209
+ recordReachableIdsFromRecord(
210
+ store,
211
+ nextRecord,
212
+ mutableRetainedIds,
213
+ selection.selections,
214
+ variables,
215
+ );
216
+ }
217
+ }
218
+
219
+ continue;
220
+ case 'Scalar':
221
+ continue;
222
+ }
223
+ }
224
+ }
225
+
226
+ type DidUnretainSomeQuery = boolean;
227
+ export function unretainQuery(
228
+ environment: IsographEnvironment,
229
+ retainedQuery: RetainedQuery,
230
+ ): DidUnretainSomeQuery {
231
+ environment.retainedQueries.delete(retainedQuery);
232
+ environment.gcBuffer.push(retainedQuery);
233
+
234
+ if (environment.gcBuffer.length > environment.gcBufferSize) {
235
+ environment.gcBuffer.shift();
236
+ return true;
237
+ }
238
+
239
+ return false;
240
+ }
241
+
242
+ export function retainQuery(
243
+ environment: IsographEnvironment,
244
+ queryToRetain: RetainedQuery,
245
+ ) {
246
+ environment.retainedQueries.add(queryToRetain);
247
+ // TODO can we remove this query from the buffer somehow?
248
+ // We are relying on === equality, but we really should be comparing
249
+ // id + variables
250
+ }
package/src/cache.tsx CHANGED
@@ -21,6 +21,10 @@ import {
21
21
  StoreRecord,
22
22
  Link,
23
23
  type IsographEnvironment,
24
+ garbageCollectEnvironment,
25
+ RetainedQuery,
26
+ unretainQuery,
27
+ retainQuery,
24
28
  } from './IsographEnvironment';
25
29
 
26
30
  declare global {
@@ -83,6 +87,18 @@ export function getOrCreateCacheForArtifact<T>(
83
87
  return getOrCreateCache<PromiseWrapper<T>>(environment, cacheKey, factory);
84
88
  }
85
89
 
90
+ type NetworkRequestStatus =
91
+ | {
92
+ kind: 'UndisposedIncomplete';
93
+ }
94
+ | {
95
+ kind: 'Disposed';
96
+ }
97
+ | {
98
+ kind: 'UndisposedComplete';
99
+ retainedQuery: RetainedQuery;
100
+ };
101
+
86
102
  export function makeNetworkRequest<T>(
87
103
  environment: IsographEnvironment,
88
104
  artifact: IsoResolver,
@@ -91,20 +107,37 @@ export function makeNetworkRequest<T>(
91
107
  if (typeof window !== 'undefined' && window.__LOG) {
92
108
  console.log('make network request', artifact, variables);
93
109
  }
110
+ let status: NetworkRequestStatus = {
111
+ kind: 'UndisposedIncomplete',
112
+ };
113
+ // This should be an observable, not a promise
94
114
  const promise = environment
95
115
  .networkFunction(artifact.queryText, variables)
96
116
  .then((networkResponse) => {
97
117
  if (typeof window !== 'undefined' && window.__LOG) {
98
118
  console.log('network response', artifact, artifact);
99
119
  }
100
- normalizeData(
101
- environment,
102
- artifact.normalizationAst,
103
- networkResponse.data,
104
- variables,
105
- artifact.nestedRefetchQueries,
106
- );
107
- return networkResponse.data;
120
+
121
+ if (status.kind === 'UndisposedIncomplete') {
122
+ normalizeData(
123
+ environment,
124
+ artifact.normalizationAst,
125
+ networkResponse.data,
126
+ variables,
127
+ artifact.nestedRefetchQueries,
128
+ );
129
+ const retainedQuery = {
130
+ normalizationAst: artifact.normalizationAst,
131
+ variables,
132
+ };
133
+ status = {
134
+ kind: 'UndisposedComplete',
135
+ retainedQuery,
136
+ };
137
+ retainQuery(environment, retainedQuery);
138
+ }
139
+ // TODO return null
140
+ return networkResponse;
108
141
  });
109
142
 
110
143
  const wrapper = wrapPromise(promise);
@@ -112,7 +145,18 @@ export function makeNetworkRequest<T>(
112
145
  const response: ItemCleanupPair<PromiseWrapper<T>> = [
113
146
  wrapper,
114
147
  () => {
115
- // delete from cache
148
+ if (status.kind === 'UndisposedComplete') {
149
+ const didUnretainSomeQuery = unretainQuery(
150
+ environment,
151
+ status.retainedQuery,
152
+ );
153
+ if (didUnretainSomeQuery) {
154
+ garbageCollectEnvironment(environment);
155
+ }
156
+ }
157
+ status = {
158
+ kind: 'Disposed',
159
+ };
116
160
  },
117
161
  ];
118
162
  return response;
@@ -138,7 +182,9 @@ function normalizeData(
138
182
  networkResponse: NetworkResponseObject,
139
183
  variables: Object,
140
184
  nestedRefetchQueries: RefetchQueryArtifactWrapper[],
141
- ) {
185
+ ): Set<DataId> {
186
+ const encounteredIds = new Set<DataId>();
187
+
142
188
  if (typeof window !== 'undefined' && window.__LOG) {
143
189
  console.log(
144
190
  'about to normalize',
@@ -155,11 +201,13 @@ function normalizeData(
155
201
  ROOT_ID,
156
202
  variables as any,
157
203
  nestedRefetchQueries,
204
+ encounteredIds,
158
205
  );
159
206
  if (typeof window !== 'undefined' && window.__LOG) {
160
207
  console.log('after normalization', { store: environment.store });
161
208
  }
162
209
  callSubscriptions(environment);
210
+ return encounteredIds;
163
211
  }
164
212
 
165
213
  export function subscribe(
@@ -194,7 +242,9 @@ function normalizeDataIntoRecord(
194
242
  targetParentRecordId: DataId,
195
243
  variables: { [index: string]: string },
196
244
  nestedRefetchQueries: RefetchQueryArtifactWrapper[],
245
+ mutableEncounteredIds: Set<DataId>,
197
246
  ) {
247
+ mutableEncounteredIds.add(targetParentRecordId);
198
248
  for (const normalizationNode of normalizationAst) {
199
249
  switch (normalizationNode.kind) {
200
250
  case 'Scalar': {
@@ -215,6 +265,7 @@ function normalizeDataIntoRecord(
215
265
  targetParentRecordId,
216
266
  variables,
217
267
  nestedRefetchQueries,
268
+ mutableEncounteredIds,
218
269
  );
219
270
  break;
220
271
  }
@@ -253,6 +304,7 @@ function normalizeLinkedField(
253
304
  targetParentRecordId: DataId,
254
305
  variables: { [index: string]: string },
255
306
  nestedRefetchQueries: RefetchQueryArtifactWrapper[],
307
+ mutableEncounteredIds: Set<DataId>,
256
308
  ) {
257
309
  const networkResponseKey = getNetworkResponseKey(astNode);
258
310
  const networkResponseData = networkResponseParentRecord[networkResponseKey];
@@ -282,7 +334,9 @@ function normalizeLinkedField(
282
334
  variables,
283
335
  i,
284
336
  nestedRefetchQueries,
337
+ mutableEncounteredIds,
285
338
  );
339
+
286
340
  dataIds.push({ __link: newStoreRecordId });
287
341
  }
288
342
  targetParentRecord[parentRecordKey] = dataIds;
@@ -295,6 +349,7 @@ function normalizeLinkedField(
295
349
  variables,
296
350
  null,
297
351
  nestedRefetchQueries,
352
+ mutableEncounteredIds,
298
353
  );
299
354
  targetParentRecord[parentRecordKey] = {
300
355
  __link: newStoreRecordId,
@@ -310,6 +365,7 @@ function normalizeNetworkResponseObject(
310
365
  variables: { [index: string]: string },
311
366
  index: number | null,
312
367
  nestedRefetchQueries: RefetchQueryArtifactWrapper[],
368
+ mutableEncounteredIds: Set<DataId>,
313
369
  ): DataId /* The id of the modified or newly created item */ {
314
370
  const newStoreRecordId = getDataIdOfNetworkResponse(
315
371
  targetParentRecordId,
@@ -330,6 +386,7 @@ function normalizeNetworkResponseObject(
330
386
  newStoreRecordId,
331
387
  variables,
332
388
  nestedRefetchQueries,
389
+ mutableEncounteredIds,
333
390
  );
334
391
 
335
392
  return newStoreRecordId;
package/src/index.tsx CHANGED
@@ -120,7 +120,6 @@ export type ReaderMutationField = {
120
120
  export type NormalizationAstNode =
121
121
  | NormalizationScalarField
122
122
  | NormalizationLinkedField;
123
- // @ts-ignore
124
123
  export type NormalizationAst = NormalizationAstNode[];
125
124
 
126
125
  export type NormalizationScalarField = {