@isograph/react-disposable-state 0.1.1 → 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.
@@ -21,6 +21,8 @@ const CacheItem_1 = require("./CacheItem");
21
21
  * be called
22
22
  */
23
23
  class ParentCache {
24
+ // TODO pass an onEmpty function, which can e.g. remove this ParentCache
25
+ // from some parent object.
24
26
  constructor(factory) {
25
27
  this.__cacheItem = null;
26
28
  this.__factory = factory;
@@ -1,5 +1,5 @@
1
1
  import { ParentCache } from './ParentCache';
2
- import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
2
+ import { ItemCleanupPair } from '@isograph/disposable-types';
3
3
  /**
4
4
  * usePrecommitValue<T>
5
5
  * - Takes a mutable parent cache, a factory function, and an onCommit callback.
@@ -9,7 +9,7 @@ import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
9
9
  * disposed by the time of the commit. If so, this hook checks the parent cache
10
10
  * for another T or creates one, and passes this T to onCommit.
11
11
  * - If the T returned during the last render is not the same as the one that
12
- * is passed to onCommit, during the commit phase, will schedule another render.
12
+ * is passed to onCommit, during the commit phase, it will schedule another render.
13
13
  *
14
14
  * Invariant: the returned T has not been disposed during the tick of the render.
15
15
  * The T passed to the onCommit callback has not been disposed when the onCommit
@@ -12,7 +12,7 @@ const useHasCommittedRef_1 = require("./useHasCommittedRef");
12
12
  * disposed by the time of the commit. If so, this hook checks the parent cache
13
13
  * for another T or creates one, and passes this T to onCommit.
14
14
  * - If the T returned during the last render is not the same as the one that
15
- * is passed to onCommit, during the commit phase, will schedule another render.
15
+ * is passed to onCommit, during the commit phase, it will schedule another render.
16
16
  *
17
17
  * Invariant: the returned T has not been disposed during the tick of the render.
18
18
  * The T passed to the onCommit callback has not been disposed when the onCommit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isograph/react-disposable-state",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Primitives for managing disposable state in React",
5
5
  "homepage": "https://isograph.dev",
6
6
  "main": "dist/index.js",
@@ -14,11 +14,14 @@
14
14
  "test-watch": "vitest watch",
15
15
  "coverage": "vitest run --coverage",
16
16
  "note": "WE SHOULD ALSO TEST HERE",
17
- "prepack": "yarn run compile"
17
+ "prepack": "yarn run compile",
18
+ "tsc": "tsc"
18
19
  },
19
20
  "dependencies": {
20
- "@isograph/disposable-types": "0.1.1",
21
- "react": "^18.2.0"
21
+ "@isograph/disposable-types": "0.2.0"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "18.2.0"
22
25
  },
23
26
  "devDependencies": {
24
27
  "@types/react": "^18.0.31",
@@ -30,5 +33,6 @@
30
33
  "type": "git",
31
34
  "url": "git+https://github.com/isographlabs/isograph.git",
32
35
  "directory": "libs/isograph-react-disposable-state"
33
- }
36
+ },
37
+ "sideEffects": false
34
38
  }
@@ -28,6 +28,8 @@ export class ParentCache<T> {
28
28
  private __cacheItem: CacheItem<T> | null = null;
29
29
  private readonly __factory: Factory<T>;
30
30
 
31
+ // TODO pass an onEmpty function, which can e.g. remove this ParentCache
32
+ // from some parent object.
31
33
  constructor(factory: Factory<T>) {
32
34
  this.__factory = factory;
33
35
  }
@@ -60,13 +60,135 @@ async function awaitableCreate(Component, isConcurrent) {
60
60
  return element;
61
61
  }
62
62
 
63
- describe('fake describe block to make test pass', () => {
64
- test('foo', () => {});
65
- });
63
+ describe('useCachedPrecommitValue', () => {
64
+ test('on initial render, it should call getOrPopulateAndTemporaryRetain', async () => {
65
+ const disposeItem = vi.fn();
66
+ const factory = vi.fn(() => {
67
+ const pair: ItemCleanupPair<number> = [1, disposeItem];
68
+ return pair;
69
+ });
70
+ const cache = new ParentCache(factory);
71
+ const getOrPopulateAndTemporaryRetain = vi.spyOn(
72
+ cache,
73
+ 'getOrPopulateAndTemporaryRetain',
74
+ );
75
+
76
+ const componentCommits = vi.fn();
77
+ const hookOnCommit = vi.fn();
78
+ const render = vi.fn();
79
+ function TestComponent() {
80
+ render();
81
+ React.useEffect(componentCommits);
82
+
83
+ const data = useCachedPrecommitValue(cache, hookOnCommit);
84
+
85
+ expect(render).toBeCalledTimes(1);
86
+ expect(componentCommits).not.toBeCalled();
87
+ expect(hookOnCommit).not.toBeCalled();
88
+ expect(data).toEqual({ state: 1 });
89
+ expect(factory).toBeCalledTimes(1);
90
+ expect(disposeItem).not.toBeCalled();
91
+ expect(getOrPopulateAndTemporaryRetain).toBeCalledTimes(1);
92
+
93
+ // TODO we should assert that permanentRetainIfNotDisposed was called
94
+ // on the cache item.
95
+
96
+ return <div />;
97
+ }
98
+
99
+ await awaitableCreate(<TestComponent />, false);
100
+
101
+ expect(componentCommits).toBeCalledTimes(1);
102
+ expect(hookOnCommit).toBeCalledTimes(1);
103
+ expect(render).toHaveBeenCalledTimes(1);
104
+ });
105
+
106
+ test('on commit, it should call the provided callback and empty the parent cache', async () => {
107
+ const disposeItem = vi.fn();
108
+ const factory = vi.fn(() => {
109
+ const pair: ItemCleanupPair<number> = [1, disposeItem];
110
+ return pair;
111
+ });
112
+ const cache = new ParentCache(factory);
113
+
114
+ const componentCommits = vi.fn();
115
+ const hookOnCommit = vi.fn();
116
+ const render = vi.fn();
117
+ function TestComponent() {
118
+ render();
119
+ expect(render).toHaveBeenCalledTimes(1);
120
+ const data = useCachedPrecommitValue(cache, hookOnCommit);
121
+
122
+ React.useEffect(() => {
123
+ componentCommits();
124
+ expect(componentCommits).toHaveBeenCalledTimes(1);
125
+ expect(hookOnCommit).toBeCalledTimes(1);
126
+ expect(hookOnCommit.mock.calls[0][0][0]).toBe(1);
127
+ expect(typeof hookOnCommit.mock.calls[0][0][1]).toBe('function');
128
+ expect(factory).toBeCalledTimes(1);
129
+ expect(disposeItem).not.toBeCalled();
130
+ expect(cache.isEmpty()).toBe(true);
131
+ }, []);
132
+
133
+ expect(factory).toBeCalledTimes(1);
134
+ expect(disposeItem).not.toBeCalled();
135
+ return <div />;
136
+ }
137
+
138
+ await awaitableCreate(<TestComponent />, false);
139
+ expect(componentCommits).toBeCalledTimes(1);
140
+ expect(render).toHaveBeenCalledTimes(1);
141
+ });
142
+
143
+ test('after commit, on subsequent renders it should return null', async () => {
144
+ const disposeItem = vi.fn();
145
+ const factory = vi.fn(() => {
146
+ const pair: ItemCleanupPair<number> = [1, disposeItem];
147
+ return pair;
148
+ });
149
+ const cache = new ParentCache(factory);
150
+
151
+ const componentCommits = vi.fn();
152
+ const hookOnCommit = vi.fn();
153
+ let setState;
154
+ let initialRender = true;
155
+ function TestComponent() {
156
+ const [, _setState] = React.useState(null);
157
+ setState = _setState;
158
+ const value = useCachedPrecommitValue(cache, hookOnCommit);
159
+
160
+ if (initialRender && value !== null) {
161
+ initialRender = false;
162
+ expect(value).toEqual({ state: 1 });
163
+ } else {
164
+ expect(value).toEqual(null);
165
+ }
66
166
 
67
- if (false) {
68
- describe('useCachedPrecommitValue', () => {
69
- test('on initial render, it should call getOrPopulateAndTemporaryRetain', async () => {
167
+ React.useEffect(() => {
168
+ componentCommits();
169
+ expect(componentCommits).toHaveBeenCalledTimes(1);
170
+ expect(hookOnCommit).toBeCalledTimes(1);
171
+ expect(factory).toBeCalledTimes(1);
172
+ expect(disposeItem).not.toBeCalled();
173
+ }, []);
174
+
175
+ return <div />;
176
+ }
177
+
178
+ await awaitableCreate(<TestComponent />, false);
179
+
180
+ expect(componentCommits).toHaveBeenCalledTimes(1);
181
+
182
+ // Trigger a re-render
183
+ setState({});
184
+ await shortPromise();
185
+ expect(initialRender).toBe(false);
186
+ });
187
+
188
+ test(
189
+ 'on repeated pre-commit renders, if the temporary retain is not disposed, ' +
190
+ 'it should re-call getOrPopulateAndTemporaryRetain but not call factory again',
191
+ async () => {
70
192
  const disposeItem = vi.fn();
71
193
  const factory = vi.fn(() => {
72
194
  const pair: ItemCleanupPair<number> = [1, disposeItem];
@@ -81,497 +203,369 @@ if (false) {
81
203
  const componentCommits = vi.fn();
82
204
  const hookOnCommit = vi.fn();
83
205
  const render = vi.fn();
206
+ let renderCount = 0;
84
207
  function TestComponent() {
85
208
  render();
86
- React.useEffect(componentCommits);
209
+ const value = useCachedPrecommitValue(cache, hookOnCommit);
87
210
 
88
- const data = useCachedPrecommitValue(cache, hookOnCommit);
211
+ expect(value).toEqual({ state: 1 });
212
+ expect(factory).toHaveBeenCalledTimes(1);
89
213
 
90
- expect(render).toBeCalledTimes(1);
91
- expect(componentCommits).not.toBeCalled();
92
- expect(hookOnCommit).not.toBeCalled();
93
- expect(data).toEqual({ state: 1 });
94
- expect(factory).toBeCalledTimes(1);
95
- expect(disposeItem).not.toBeCalled();
96
- expect(getOrPopulateAndTemporaryRetain).toBeCalledTimes(1);
214
+ renderCount++;
215
+ expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(
216
+ renderCount,
217
+ );
97
218
 
98
- // TODO we should assert that permanentRetainIfNotDisposed was called
99
- // on the cache item.
219
+ React.useEffect(() => {
220
+ componentCommits();
221
+ expect(componentCommits).toHaveBeenCalledTimes(1);
222
+ expect(hookOnCommit).toBeCalledTimes(1);
223
+ expect(factory).toBeCalledTimes(1);
224
+ expect(disposeItem).not.toBeCalled();
225
+ }, []);
100
226
 
101
227
  return <div />;
102
228
  }
103
229
 
104
- await awaitableCreate(<TestComponent />, false);
230
+ const { promise, isResolvedRef, resolve } = promiseAndResolver();
231
+ await awaitableCreate(
232
+ <React.Suspense fallback={<div />}>
233
+ <TestComponent />
234
+ <Suspender promise={promise} isResolvedRef={isResolvedRef} />
235
+ </React.Suspense>,
236
+ true,
237
+ );
105
238
 
106
- expect(componentCommits).toBeCalledTimes(1);
107
- expect(hookOnCommit).toBeCalledTimes(1);
239
+ expect(componentCommits).toHaveBeenCalledTimes(0);
108
240
  expect(render).toHaveBeenCalledTimes(1);
109
- });
110
241
 
111
- test('on commit, it should call the provided callback and empty the parent cache', async () => {
242
+ resolve();
243
+ await shortPromise();
244
+
245
+ expect(componentCommits).toHaveBeenCalledTimes(1);
246
+ expect(render).toHaveBeenCalledTimes(2);
247
+ },
248
+ );
249
+
250
+ test(
251
+ 'on repeated pre-commit renders, if the temporary retain is disposed, ' +
252
+ 'it should re-call getOrPopulateAndTemporaryRetain and factory',
253
+ async () => {
112
254
  const disposeItem = vi.fn();
255
+ let factoryValue = 0;
113
256
  const factory = vi.fn(() => {
114
- const pair: ItemCleanupPair<number> = [1, disposeItem];
257
+ factoryValue++;
258
+ const pair: ItemCleanupPair<number> = [factoryValue, disposeItem];
115
259
  return pair;
116
260
  });
117
261
  const cache = new ParentCache(factory);
118
262
 
263
+ const getOrPopulateAndTemporaryRetain = vi.spyOn(
264
+ cache,
265
+ 'getOrPopulateAndTemporaryRetain',
266
+ );
267
+
119
268
  const componentCommits = vi.fn();
120
269
  const hookOnCommit = vi.fn();
121
270
  const render = vi.fn();
122
271
  function TestComponent() {
123
272
  render();
124
- expect(render).toHaveBeenCalledTimes(1);
125
- const data = useCachedPrecommitValue(cache, hookOnCommit);
273
+ const value = useCachedPrecommitValue(cache, hookOnCommit);
274
+
275
+ expect(value).toEqual({ state: factoryValue });
126
276
 
127
277
  React.useEffect(() => {
128
278
  componentCommits();
279
+ expect(cache.isEmpty()).toBe(true);
129
280
  expect(componentCommits).toHaveBeenCalledTimes(1);
130
281
  expect(hookOnCommit).toBeCalledTimes(1);
131
- expect(hookOnCommit.mock.calls[0][0][0]).toBe(1);
132
- expect(typeof hookOnCommit.mock.calls[0][0][1]).toBe('function');
133
- expect(factory).toBeCalledTimes(1);
134
- expect(disposeItem).not.toBeCalled();
135
- expect(cache.isEmpty()).toBe(true);
282
+ expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
283
+ expect(factory).toBeCalledTimes(2);
284
+ expect(disposeItem).toBeCalledTimes(1);
136
285
  }, []);
137
286
 
138
- expect(factory).toBeCalledTimes(1);
139
- expect(disposeItem).not.toBeCalled();
287
+ if (render.mock.calls.length === 1) {
288
+ expect(factory).toHaveBeenCalledTimes(1);
289
+ // First render, dispose the temporary retain
290
+ expect(disposeItem).toBeCalledTimes(0);
291
+ getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
292
+ expect(disposeItem).toBeCalledTimes(1);
293
+ expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
294
+ } else {
295
+ expect(factory).toHaveBeenCalledTimes(2);
296
+ expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
297
+ }
298
+
140
299
  return <div />;
141
300
  }
142
301
 
143
- await awaitableCreate(<TestComponent />, false);
144
- expect(componentCommits).toBeCalledTimes(1);
302
+ const { promise, isResolvedRef, resolve } = promiseAndResolver();
303
+ await awaitableCreate(
304
+ <React.Suspense fallback={<div />}>
305
+ <TestComponent />
306
+ <Suspender promise={promise} isResolvedRef={isResolvedRef} />
307
+ </React.Suspense>,
308
+ true,
309
+ );
310
+
311
+ expect(componentCommits).toHaveBeenCalledTimes(0);
145
312
  expect(render).toHaveBeenCalledTimes(1);
146
- });
147
313
 
148
- test('after commit, on subsequent renders it should return null', async () => {
314
+ resolve();
315
+ await shortPromise();
316
+
317
+ expect(componentCommits).toHaveBeenCalledTimes(1);
318
+ expect(render).toHaveBeenCalledTimes(2);
319
+ },
320
+ );
321
+
322
+ test(
323
+ 'if the item has been disposed between the render and the commit, ' +
324
+ 'and the parent cache is empty, it will call factory again, re-render an ' +
325
+ 'additional time and called onCommit with the newly generated item',
326
+ async () => {
149
327
  const disposeItem = vi.fn();
328
+ let factoryCount = 0;
150
329
  const factory = vi.fn(() => {
151
- const pair: ItemCleanupPair<number> = [1, disposeItem];
330
+ factoryCount++;
331
+ const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
152
332
  return pair;
153
333
  });
154
334
  const cache = new ParentCache(factory);
335
+ const getOrPopulateAndTemporaryRetain = vi.spyOn(
336
+ cache,
337
+ 'getOrPopulateAndTemporaryRetain',
338
+ );
339
+ const getAndPermanentRetainIfPresent = vi.spyOn(
340
+ cache,
341
+ 'getAndPermanentRetainIfPresent',
342
+ );
155
343
 
156
344
  const componentCommits = vi.fn();
157
345
  const hookOnCommit = vi.fn();
158
- let setState;
159
- let initialRender = true;
346
+ const render = vi.fn();
160
347
  function TestComponent() {
161
- const [, _setState] = React.useState(null);
162
- setState = _setState;
163
- const value = useCachedPrecommitValue(cache, hookOnCommit);
348
+ render();
164
349
 
165
- if (initialRender && value !== null) {
166
- initialRender = false;
167
- expect(value).toEqual({ state: 1 });
168
- } else {
169
- expect(value).toEqual(null);
170
- }
350
+ useCachedPrecommitValue(cache, hookOnCommit);
171
351
 
172
352
  React.useEffect(() => {
173
353
  componentCommits();
174
- expect(componentCommits).toHaveBeenCalledTimes(1);
175
- expect(hookOnCommit).toBeCalledTimes(1);
176
- expect(factory).toBeCalledTimes(1);
177
- expect(disposeItem).not.toBeCalled();
354
+ expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
355
+ expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
356
+ expect(getAndPermanentRetainIfPresent.mock.results[0].value).toBe(
357
+ null,
358
+ );
359
+ expect(factory).toHaveBeenCalledTimes(2);
360
+ expect(cache.isEmpty()).toBe(true);
361
+ expect(hookOnCommit).toHaveBeenCalledTimes(1);
362
+ expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
178
363
  }, []);
179
364
 
180
365
  return <div />;
181
366
  }
182
367
 
183
- await awaitableCreate(<TestComponent />, false);
184
-
185
- expect(componentCommits).toHaveBeenCalledTimes(1);
186
-
187
- // Trigger a re-render
188
- setState({});
189
- await shortPromise();
190
- expect(initialRender).toBe(false);
191
- });
192
-
193
- test(
194
- 'on repeated pre-commit renders, if the temporary retain is not disposed, ' +
195
- 'it should re-call getOrPopulateAndTemporaryRetain but not call factory again',
196
- async () => {
197
- const disposeItem = vi.fn();
198
- const factory = vi.fn(() => {
199
- const pair: ItemCleanupPair<number> = [1, disposeItem];
200
- return pair;
201
- });
202
- const cache = new ParentCache(factory);
203
- const getOrPopulateAndTemporaryRetain = vi.spyOn(
204
- cache,
205
- 'getOrPopulateAndTemporaryRetain',
206
- );
207
-
208
- const componentCommits = vi.fn();
209
- const hookOnCommit = vi.fn();
210
- const render = vi.fn();
211
- let renderCount = 0;
212
- function TestComponent() {
213
- render();
214
- const value = useCachedPrecommitValue(cache, hookOnCommit);
368
+ // wat is going on?
369
+ //
370
+ // We want to test a scenario where the item is disposed between the render and
371
+ // the commit.
372
+ //
373
+ // The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
374
+ //
375
+ // - During TestComponent's render, it will populate the cache.
376
+ // - Then, CodeExecutor will render, and dispose the temporary retain,
377
+ // disposing the cache item. The parent cache will be empty as well.
378
+ // - Then, TestComponent commits.
379
+ let initialRender = true;
380
+ function CodeExecutor() {
381
+ if (initialRender) {
382
+ // This code executes after the initial render of TestComponent, but before
383
+ // it commits.
384
+ expect(disposeItem).not.toHaveBeenCalled();
385
+ getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
386
+ expect(disposeItem).toHaveBeenCalledTimes(1);
387
+ expect(cache.isEmpty()).toBe(true);
215
388
 
216
- expect(value).toEqual({ state: 1 });
389
+ expect(render).toHaveBeenCalledTimes(1);
390
+ expect(hookOnCommit).toBeCalledTimes(0);
391
+ expect(componentCommits).toBeCalledTimes(0);
217
392
  expect(factory).toHaveBeenCalledTimes(1);
218
393
 
219
- renderCount++;
220
- expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(
221
- renderCount,
222
- );
223
-
224
- React.useEffect(() => {
225
- componentCommits();
226
- expect(componentCommits).toHaveBeenCalledTimes(1);
227
- expect(hookOnCommit).toBeCalledTimes(1);
228
- expect(factory).toBeCalledTimes(1);
229
- expect(disposeItem).not.toBeCalled();
230
- }, []);
231
-
232
- return <div />;
394
+ initialRender = false;
233
395
  }
234
396
 
235
- const { promise, isResolvedRef, resolve } = promiseAndResolver();
236
- await awaitableCreate(
237
- <React.Suspense fallback={<div />}>
238
- <TestComponent />
239
- <Suspender promise={promise} isResolvedRef={isResolvedRef} />
240
- </React.Suspense>,
241
- true,
242
- );
243
-
244
- expect(componentCommits).toHaveBeenCalledTimes(0);
245
- expect(render).toHaveBeenCalledTimes(1);
397
+ return null;
398
+ }
246
399
 
247
- resolve();
248
- await shortPromise();
400
+ const element = await awaitableCreate(
401
+ <>
402
+ <TestComponent />
403
+ <CodeExecutor />
404
+ </>,
405
+ false,
406
+ );
249
407
 
250
- expect(componentCommits).toHaveBeenCalledTimes(1);
251
- expect(render).toHaveBeenCalledTimes(2);
252
- },
253
- );
408
+ // This code executes after the commit and re-render of TestComponent.
409
+ // The commit triggers a re-render, because the item was disposed.
410
+ expect(render).toHaveBeenCalledTimes(2);
411
+ expect(factory).toBeCalledTimes(2);
412
+ },
413
+ );
254
414
 
255
- test(
256
- 'on repeated pre-commit renders, if the temporary retain is disposed, ' +
257
- 'it should re-call getOrPopulateAndTemporaryRetain and factory',
258
- async () => {
259
- const disposeItem = vi.fn();
260
- let factoryValue = 0;
261
- const factory = vi.fn(() => {
262
- factoryValue++;
263
- const pair: ItemCleanupPair<number> = [factoryValue, disposeItem];
264
- return pair;
265
- });
266
- const cache = new ParentCache(factory);
267
-
268
- const getOrPopulateAndTemporaryRetain = vi.spyOn(
269
- cache,
270
- 'getOrPopulateAndTemporaryRetain',
271
- );
415
+ test(
416
+ 'if, between the render and the commit, the item has been disposed, ' +
417
+ 'and the parent cache is not empty, it will not call factory again, will re-render ' +
418
+ 'an additional time and will call onCommit with the value in the parent cache',
419
+ async () => {
420
+ const disposeItem = vi.fn();
421
+ let factoryCount = 0;
422
+ const factory = vi.fn(() => {
423
+ factoryCount++;
424
+ const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
425
+ return pair;
426
+ });
427
+ const cache = new ParentCache(factory);
428
+ const getAndPermanentRetainIfPresent = vi.spyOn(
429
+ cache,
430
+ 'getAndPermanentRetainIfPresent',
431
+ );
272
432
 
273
- const componentCommits = vi.fn();
274
- const hookOnCommit = vi.fn();
275
- const render = vi.fn();
276
- function TestComponent() {
277
- render();
278
- const value = useCachedPrecommitValue(cache, hookOnCommit);
279
-
280
- expect(value).toEqual({ state: factoryValue });
281
-
282
- React.useEffect(() => {
283
- componentCommits();
284
- expect(cache.isEmpty()).toBe(true);
285
- expect(componentCommits).toHaveBeenCalledTimes(1);
286
- expect(hookOnCommit).toBeCalledTimes(1);
287
- expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
288
- expect(factory).toBeCalledTimes(2);
289
- expect(disposeItem).toBeCalledTimes(1);
290
- }, []);
291
-
292
- if (render.mock.calls.length === 1) {
293
- expect(factory).toHaveBeenCalledTimes(1);
294
- // First render, dispose the temporary retain
295
- expect(disposeItem).toBeCalledTimes(0);
296
- getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
297
- expect(disposeItem).toBeCalledTimes(1);
298
- expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
299
- } else {
300
- expect(factory).toHaveBeenCalledTimes(2);
301
- expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
302
- }
303
-
304
- return <div />;
305
- }
433
+ const componentCommits = vi.fn();
434
+ const hookOnCommit = vi.fn();
435
+ const render = vi.fn();
436
+ function TestComponent() {
437
+ render();
438
+ useCachedPrecommitValue(cache, hookOnCommit);
306
439
 
307
- const { promise, isResolvedRef, resolve } = promiseAndResolver();
308
- await awaitableCreate(
309
- <React.Suspense fallback={<div />}>
310
- <TestComponent />
311
- <Suspender promise={promise} isResolvedRef={isResolvedRef} />
312
- </React.Suspense>,
313
- true,
314
- );
440
+ React.useEffect(() => {
441
+ componentCommits();
442
+ // Note that we called getOrPopulateAndTemporaryRetain during CodeExecutor, hence 2
443
+ expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
444
+ expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
445
+ expect(getAndPermanentRetainIfPresent.mock.results[0].value[0]).toBe(
446
+ 2,
447
+ );
448
+ expect(factory).toHaveBeenCalledTimes(2);
449
+ expect(hookOnCommit).toHaveBeenCalledTimes(1);
450
+ expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
451
+ }, []);
315
452
 
316
- expect(componentCommits).toHaveBeenCalledTimes(0);
317
- expect(render).toHaveBeenCalledTimes(1);
453
+ return <div />;
454
+ }
318
455
 
319
- resolve();
320
- await shortPromise();
456
+ const getOrPopulateAndTemporaryRetain = vi.spyOn(
457
+ cache,
458
+ 'getOrPopulateAndTemporaryRetain',
459
+ );
321
460
 
322
- expect(componentCommits).toHaveBeenCalledTimes(1);
323
- expect(render).toHaveBeenCalledTimes(2);
324
- },
325
- );
461
+ // wat is going on?
462
+ //
463
+ // We want to test a scenario where the item is disposed between the render and
464
+ // the commit.
465
+ //
466
+ // The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
467
+ //
468
+ // - During TestComponent's render, it will populate the cache.
469
+ // - Then, CodeExecutor will render, and dispose the temporary retain,
470
+ // disposing the cache item. It will then repopulate the parent cache.
471
+ // - Then, TestComponent commits.
472
+ let initialRender = true;
473
+ function CodeExecutor() {
474
+ if (initialRender) {
475
+ // This code executes after the initial render of TestComponent, but before
476
+ // it commits.
477
+ expect(disposeItem).not.toHaveBeenCalled();
478
+ getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
479
+ expect(disposeItem).toHaveBeenCalledTimes(1);
480
+ expect(cache.isEmpty()).toBe(true);
326
481
 
327
- test(
328
- 'if the item has been disposed between the render and the commit, ' +
329
- 'and the parent cache is empty, it will call factory again, re-render an ' +
330
- 'additional time and called onCommit with the newly generated item',
331
- async () => {
332
- const disposeItem = vi.fn();
333
- let factoryCount = 0;
334
- const factory = vi.fn(() => {
335
- factoryCount++;
336
- const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
337
- return pair;
338
- });
339
- const cache = new ParentCache(factory);
340
- const getOrPopulateAndTemporaryRetain = vi.spyOn(
341
- cache,
342
- 'getOrPopulateAndTemporaryRetain',
343
- );
344
- const getAndPermanentRetainIfPresent = vi.spyOn(
345
- cache,
346
- 'getAndPermanentRetainIfPresent',
347
- );
482
+ cache.getOrPopulateAndTemporaryRetain();
483
+ expect(cache.isEmpty()).toBe(false);
484
+ // The factory function was called when we called getOrPopulateAndTemporaryRetain
485
+ expect(factory).toHaveBeenCalledTimes(2);
348
486
 
349
- const componentCommits = vi.fn();
350
- const hookOnCommit = vi.fn();
351
- const render = vi.fn();
352
- function TestComponent() {
353
- render();
354
-
355
- useCachedPrecommitValue(cache, hookOnCommit);
356
-
357
- React.useEffect(() => {
358
- componentCommits();
359
- expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
360
- expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
361
- expect(getAndPermanentRetainIfPresent.mock.results[0].value).toBe(
362
- null,
363
- );
364
- expect(factory).toHaveBeenCalledTimes(2);
365
- expect(cache.isEmpty()).toBe(true);
366
- expect(hookOnCommit).toHaveBeenCalledTimes(1);
367
- expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
368
- }, []);
369
-
370
- return <div />;
371
- }
487
+ expect(render).toHaveBeenCalledTimes(1);
488
+ expect(hookOnCommit).toBeCalledTimes(0);
489
+ expect(componentCommits).toBeCalledTimes(0);
372
490
 
373
- // wat is going on?
374
- //
375
- // We want to test a scenario where the item is disposed between the render and
376
- // the commit.
377
- //
378
- // The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
379
- //
380
- // - During TestComponent's render, it will populate the cache.
381
- // - Then, CodeExecutor will render, and dispose the temporary retain,
382
- // disposing the cache item. The parent cache will be empty as well.
383
- // - Then, TestComponent commits.
384
- let initialRender = true;
385
- function CodeExecutor() {
386
- if (initialRender) {
387
- // This code executes after the initial render of TestComponent, but before
388
- // it commits.
389
- expect(disposeItem).not.toHaveBeenCalled();
390
- getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
391
- expect(disposeItem).toHaveBeenCalledTimes(1);
392
- expect(cache.isEmpty()).toBe(true);
393
-
394
- expect(render).toHaveBeenCalledTimes(1);
395
- expect(hookOnCommit).toBeCalledTimes(0);
396
- expect(componentCommits).toBeCalledTimes(0);
397
- expect(factory).toHaveBeenCalledTimes(1);
398
-
399
- initialRender = false;
400
- }
401
-
402
- return null;
491
+ initialRender = false;
403
492
  }
404
493
 
405
- const element = await awaitableCreate(
406
- <>
407
- <TestComponent />
408
- <CodeExecutor />
409
- </>,
410
- false,
411
- );
412
-
413
- // This code executes after the commit and re-render of TestComponent.
414
- // The commit triggers a re-render, because the item was disposed.
415
- expect(render).toHaveBeenCalledTimes(2);
416
- expect(factory).toBeCalledTimes(2);
417
- },
418
- );
494
+ return null;
495
+ }
419
496
 
420
- test(
421
- 'if, between the render and the commit, the item has been disposed, ' +
422
- 'and the parent cache is not empty, it will not call factory again, will re-render ' +
423
- 'an additional time and will call onCommit with the value in the parent cache',
424
- async () => {
425
- const disposeItem = vi.fn();
426
- let factoryCount = 0;
427
- const factory = vi.fn(() => {
428
- factoryCount++;
429
- const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
430
- return pair;
431
- });
432
- const cache = new ParentCache(factory);
433
- const getAndPermanentRetainIfPresent = vi.spyOn(
434
- cache,
435
- 'getAndPermanentRetainIfPresent',
436
- );
497
+ const element = await awaitableCreate(
498
+ <React.Suspense fallback="fallback">
499
+ <TestComponent />
500
+ <CodeExecutor />
501
+ </React.Suspense>,
502
+ false,
503
+ );
437
504
 
438
- const componentCommits = vi.fn();
439
- const hookOnCommit = vi.fn();
440
- const render = vi.fn();
441
- function TestComponent() {
442
- render();
443
- useCachedPrecommitValue(cache, hookOnCommit);
444
-
445
- React.useEffect(() => {
446
- componentCommits();
447
- // Note that we called getOrPopulateAndTemporaryRetain during CodeExecutor, hence 2
448
- expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
449
- expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
450
- expect(
451
- getAndPermanentRetainIfPresent.mock.results[0].value[0],
452
- ).toBe(2);
453
- expect(factory).toHaveBeenCalledTimes(2);
454
- expect(hookOnCommit).toHaveBeenCalledTimes(1);
455
- expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
456
- }, []);
457
-
458
- return <div />;
459
- }
505
+ // This code executes after the commit and re-render of TestComponent.
506
+ // The commit triggers a re-render, because the item was disposed.
507
+ expect(render).toHaveBeenCalledTimes(2);
508
+ // Note that this is the same number of calls as inside of CodeExecutor,
509
+ // implying that the factory function was not called again.
510
+ expect(factory).toBeCalledTimes(2);
511
+ },
512
+ );
460
513
 
461
- const getOrPopulateAndTemporaryRetain = vi.spyOn(
462
- cache,
463
- 'getOrPopulateAndTemporaryRetain',
464
- );
514
+ test(
515
+ 'After render but before commit, the item will ' +
516
+ 'be in the parent cache, temporarily retained',
517
+ async () => {
518
+ const disposeItem = vi.fn();
519
+ const factory = vi.fn(() => {
520
+ const pair: ItemCleanupPair<number> = [1, disposeItem];
521
+ return pair;
522
+ });
523
+ const cache = new ParentCache(factory);
465
524
 
466
- // wat is going on?
467
- //
468
- // We want to test a scenario where the item is disposed between the render and
469
- // the commit.
470
- //
471
- // The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
472
- //
473
- // - During TestComponent's render, it will populate the cache.
474
- // - Then, CodeExecutor will render, and dispose the temporary retain,
475
- // disposing the cache item. It will then repopulate the parent cache.
476
- // - Then, TestComponent commits.
477
- let initialRender = true;
478
- function CodeExecutor() {
479
- if (initialRender) {
480
- // This code executes after the initial render of TestComponent, but before
481
- // it commits.
482
- expect(disposeItem).not.toHaveBeenCalled();
483
- getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
484
- expect(disposeItem).toHaveBeenCalledTimes(1);
485
- expect(cache.isEmpty()).toBe(true);
486
-
487
- cache.getOrPopulateAndTemporaryRetain();
488
- expect(cache.isEmpty()).toBe(false);
489
- // The factory function was called when we called getOrPopulateAndTemporaryRetain
490
- expect(factory).toHaveBeenCalledTimes(2);
491
-
492
- expect(render).toHaveBeenCalledTimes(1);
493
- expect(hookOnCommit).toBeCalledTimes(0);
494
- expect(componentCommits).toBeCalledTimes(0);
495
-
496
- initialRender = false;
497
- }
498
-
499
- return null;
500
- }
525
+ const componentCommits = vi.fn();
526
+ const hookOnCommit = vi.fn();
527
+ const render = vi.fn();
528
+ function TestComponent() {
529
+ render();
501
530
 
502
- const element = await awaitableCreate(
503
- <React.Suspense fallback="fallback">
504
- <TestComponent />
505
- <CodeExecutor />
506
- </React.Suspense>,
507
- false,
508
- );
531
+ useCachedPrecommitValue(cache, hookOnCommit);
509
532
 
510
- // This code executes after the commit and re-render of TestComponent.
511
- // The commit triggers a re-render, because the item was disposed.
512
- expect(render).toHaveBeenCalledTimes(2);
513
- // Note that this is the same number of calls as inside of CodeExecutor,
514
- // implying that the factory function was not called again.
515
- expect(factory).toBeCalledTimes(2);
516
- },
517
- );
533
+ React.useEffect(() => {
534
+ componentCommits();
535
+ }, []);
518
536
 
519
- test(
520
- 'After render but before commit, the item will ' +
521
- 'be in the parent cache, temporarily retained',
522
- async () => {
523
- const disposeItem = vi.fn();
524
- const factory = vi.fn(() => {
525
- const pair: ItemCleanupPair<number> = [1, disposeItem];
526
- return pair;
527
- });
528
- const cache = new ParentCache(factory);
529
-
530
- const componentCommits = vi.fn();
531
- const hookOnCommit = vi.fn();
532
- const render = vi.fn();
533
- function TestComponent() {
534
- render();
535
-
536
- useCachedPrecommitValue(cache, hookOnCommit);
537
-
538
- React.useEffect(() => {
539
- componentCommits();
540
- }, []);
541
-
542
- return <div />;
543
- }
537
+ return <div />;
538
+ }
544
539
 
545
- // wat is going on?
546
- //
547
- // We want to test a scenario where the component unmounts before committing.
548
- // However, we cannot distinguish between an unmount before commit and a
549
- // render and a commit that hasn't happened yet.
550
- //
551
- // This can be simulated with suspense.
552
- //
553
- // This test and 'on initial render, it should call getOrPopulateAndTemporaryRetain'
554
- // can be merged
555
-
556
- const { promise, isResolvedRef } = promiseAndResolver();
557
- const element = await awaitableCreate(
558
- <React.Suspense fallback={null}>
559
- <TestComponent />
560
- <Suspender promise={promise} isResolvedRef={isResolvedRef} />
561
- </React.Suspense>,
562
- true,
563
- );
540
+ // wat is going on?
541
+ //
542
+ // We want to test a scenario where the component unmounts before committing.
543
+ // However, we cannot distinguish between an unmount before commit and a
544
+ // render and a commit that hasn't happened yet.
545
+ //
546
+ // This can be simulated with suspense.
547
+ //
548
+ // This test and 'on initial render, it should call getOrPopulateAndTemporaryRetain'
549
+ // can be merged
550
+
551
+ const { promise, isResolvedRef } = promiseAndResolver();
552
+ const element = await awaitableCreate(
553
+ <React.Suspense fallback={null}>
554
+ <TestComponent />
555
+ <Suspender promise={promise} isResolvedRef={isResolvedRef} />
556
+ </React.Suspense>,
557
+ true,
558
+ );
564
559
 
565
- // This code executes after the commit and re-render of TestComponent.
566
- // The commit triggers a re-render, because the item was disposed.
567
- expect(render).toHaveBeenCalledTimes(1);
568
- expect(componentCommits).toHaveBeenCalledTimes(0);
569
- const item = getItem(cache)!;
570
- const state = getState(item);
571
- assert(state.kind === 'InParentCacheAndNotDisposed');
572
- expect(state.permanentRetainCount).toBe(0);
573
- expect(state.temporaryRetainCount).toBe(1);
574
- },
575
- );
576
- });
577
- }
560
+ // This code executes after the commit and re-render of TestComponent.
561
+ // The commit triggers a re-render, because the item was disposed.
562
+ expect(render).toHaveBeenCalledTimes(1);
563
+ expect(componentCommits).toHaveBeenCalledTimes(0);
564
+ const item = getItem(cache)!;
565
+ const state = getState(item);
566
+ assert(state.kind === 'InParentCacheAndNotDisposed');
567
+ expect(state.permanentRetainCount).toBe(0);
568
+ expect(state.temporaryRetainCount).toBe(1);
569
+ },
570
+ );
571
+ });
@@ -3,7 +3,7 @@
3
3
  import { useEffect, useState } from 'react';
4
4
  import { ParentCache } from './ParentCache';
5
5
  import { useHasCommittedRef } from './useHasCommittedRef';
6
- import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
6
+ import { ItemCleanupPair } from '@isograph/disposable-types';
7
7
 
8
8
  /**
9
9
  * usePrecommitValue<T>
@@ -14,7 +14,7 @@ import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
14
14
  * disposed by the time of the commit. If so, this hook checks the parent cache
15
15
  * for another T or creates one, and passes this T to onCommit.
16
16
  * - If the T returned during the last render is not the same as the one that
17
- * is passed to onCommit, during the commit phase, will schedule another render.
17
+ * is passed to onCommit, during the commit phase, it will schedule another render.
18
18
  *
19
19
  * Invariant: the returned T has not been disposed during the tick of the render.
20
20
  * The T passed to the onCommit callback has not been disposed when the onCommit
@@ -2,7 +2,7 @@ import { ItemCleanupPair } from '@isograph/disposable-types';
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { useHasCommittedRef } from './useHasCommittedRef';
4
4
 
5
- export const UNASSIGNED_STATE = Symbol();
5
+ export const UNASSIGNED_STATE: unique symbol = Symbol();
6
6
  export type UnassignedState = typeof UNASSIGNED_STATE;
7
7
 
8
8
  type UseUpdatableDisposableStateReturnValue<T> = {
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig.pkg.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ },
6
+ }