@isograph/react-disposable-state 0.0.0-main-54f75b55 → 0.0.0-main-36b092bd
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/.turbo/turbo-compile-libs.log +2 -2
- package/.turbo/turbo-tsc.log +1 -1
- package/README.md +47 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/useDisposableArray.cjs +20 -0
- package/dist/useDisposableArray.d.cts +11 -0
- package/dist/useDisposableArray.d.cts.map +1 -0
- package/dist/useDisposableArray.d.mts +11 -0
- package/dist/useDisposableArray.d.mts.map +1 -0
- package/dist/useDisposableArray.mjs +21 -0
- package/dist/useDisposableArray.mjs.map +1 -0
- package/package.json +4 -3
- package/src/index.ts +1 -0
- package/src/useDisposableArray.test.tsx +259 -0
- package/src/useDisposableArray.ts +55 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
../.. | WARN Unsupported engine: wanted: {"node":"24.12.0"} (current: {"node":"v24.21.0","pnpm":"10.15.0"})
|
|
2
2
|
|
|
3
|
-
> @isograph/react-disposable-state@0.0.0-main-
|
|
3
|
+
> @isograph/react-disposable-state@0.0.0-main-36b092bd compile-libs /home/runner/work/isograph/isograph/libs/isograph-react-disposable-state
|
|
4
4
|
> tsdown
|
|
5
5
|
|
|
6
6
|
[34mℹ[39m tsdown [2mv0.20.1[22m powered by rolldown [2mv1.0.0-rc.1[22m
|
|
7
7
|
[34mℹ[39m config file: [4m/home/runner/work/isograph/isograph/tsdown.config.ts[24m
|
|
8
|
-
(node:
|
|
8
|
+
(node:2909) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/isograph/isograph/tsdown.config.ts?no-cache=f0580075-e3d4-4057-84f0-b8678cf6a2ba is not specified and it doesn't parse as CommonJS.
|
|
9
9
|
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
|
|
10
10
|
To eliminate this warning, add "type": "module" to /home/runner/work/isograph/isograph/package.json.
|
|
11
11
|
(Use `node --trace-warnings ...` to show where the warning was created)
|
package/.turbo/turbo-tsc.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
../.. | WARN Unsupported engine: wanted: {"node":"24.12.0"} (current: {"node":"v24.21.0","pnpm":"10.15.0"})
|
|
2
2
|
|
|
3
|
-
> @isograph/react-disposable-state@0.0.0-main-
|
|
3
|
+
> @isograph/react-disposable-state@0.0.0-main-36b092bd tsc /home/runner/work/isograph/isograph/libs/isograph-react-disposable-state
|
|
4
4
|
> tsc
|
|
5
5
|
|
package/README.md
CHANGED
|
@@ -129,6 +129,53 @@ const {
|
|
|
129
129
|
} = useUpdatableDisposableClearableState<T>();
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
### `useDisposableArray`
|
|
133
|
+
|
|
134
|
+
A hook that holds an array of disposable items, built on `useUpdatableDisposableState`.
|
|
135
|
+
|
|
136
|
+
- Returns an `{ entries, setEntries }` object. `entries` is the array of `ItemCleanupPair`s the current render shows. It is empty until the first `setEntries` call commits. The hook owns each pair's cleanup, so do not call it.
|
|
137
|
+
- `setEntries(nextEntries)` stores `nextEntries`. Older arrays are released when a newer array commits, and the rest on unmount. Releasing an array calls the cleanup of each of its pairs once.
|
|
138
|
+
- Every pair in `nextEntries` must be acquired by the caller for that call, and the hook owns it from then on. To keep an item of `entries`, take a new reference to it, for example with `cloneIfNotDisposed()` on a `ReferenceCountedPointer` from `@isograph/reference-counted-pointer`. Never pass a pair of `entries` itself.
|
|
139
|
+
- Build `nextEntries` from the `entries` of the same render. If that render is stale, because its `entries` were released after a newer array committed, `cloneIfNotDisposed()` returns `null`. Release what you acquired and do not call `setEntries`.
|
|
140
|
+
- Two `setEntries` calls before a commit both build on the same `entries`, and the second replaces the first.
|
|
141
|
+
- `setEntries` throws if called before the initial commit, as `setState` does. It stores nothing in that case, so the caller still owns `nextEntries`.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const {
|
|
145
|
+
entries,
|
|
146
|
+
setEntries,
|
|
147
|
+
}: {
|
|
148
|
+
entries: ReadonlyArray<ItemCleanupPair<T>>;
|
|
149
|
+
setEntries: (nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => void;
|
|
150
|
+
} = useDisposableArray<T>();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For example, to keep at most three items, store reference-counted pointers, take a new reference to each item that stays, and add the new one:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const { entries, setEntries } =
|
|
157
|
+
useDisposableArray<ReferenceCountedPointer<Item>>();
|
|
158
|
+
|
|
159
|
+
function addItem(pair: ItemCleanupPair<Item>) {
|
|
160
|
+
const kept: Array<ItemCleanupPair<ReferenceCountedPointer<Item>>> = [];
|
|
161
|
+
for (const entry of entries.slice(-2)) {
|
|
162
|
+
const clone = entry[0].cloneIfNotDisposed();
|
|
163
|
+
if (clone == null) {
|
|
164
|
+
// This render is stale.
|
|
165
|
+
for (const keptEntry of kept) {
|
|
166
|
+
keptEntry[1]();
|
|
167
|
+
}
|
|
168
|
+
pair[1]();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
kept.push(clone);
|
|
172
|
+
}
|
|
173
|
+
setEntries([...kept, createReferenceCountedPointer(pair)]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Read an item with entries[index][0].getItemIfNotDisposed().
|
|
177
|
+
```
|
|
178
|
+
|
|
132
179
|
### `useDisposableState`
|
|
133
180
|
|
|
134
181
|
> This could properly be called `useLazyUpdatableDisposableState`, but that's quite long!
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const require_useEffectsRerunWithoutRender = require('./useEffectsRerunWithoutRe
|
|
|
4
4
|
const require_useCachedResponsivePrecommitValue = require('./useCachedResponsivePrecommitValue.cjs');
|
|
5
5
|
const require_useHasCommittedRef = require('./useHasCommittedRef.cjs');
|
|
6
6
|
const require_useUpdatableDisposableState = require('./useUpdatableDisposableState.cjs');
|
|
7
|
+
const require_useDisposableArray = require('./useDisposableArray.cjs');
|
|
7
8
|
const require_useDisposableState = require('./useDisposableState.cjs');
|
|
8
9
|
const require_useLazyDisposableState = require('./useLazyDisposableState.cjs');
|
|
9
10
|
const require_useUpdatableDisposableClearableState = require('./useUpdatableDisposableClearableState.cjs');
|
|
@@ -13,6 +14,7 @@ exports.ParentCache = require_ParentCache.ParentCache;
|
|
|
13
14
|
exports.UNASSIGNED_STATE = require_useUpdatableDisposableState.UNASSIGNED_STATE;
|
|
14
15
|
exports.createTemporarilyRetainedCacheItem = require_CacheItem.createTemporarilyRetainedCacheItem;
|
|
15
16
|
exports.useCachedResponsivePrecommitValue = require_useCachedResponsivePrecommitValue.useCachedResponsivePrecommitValue;
|
|
17
|
+
exports.useDisposableArray = require_useDisposableArray.useDisposableArray;
|
|
16
18
|
exports.useDisposableState = require_useDisposableState.useDisposableState;
|
|
17
19
|
exports.useEffectsRerunWithoutRenderRef = require_useEffectsRerunWithoutRender.useEffectsRerunWithoutRenderRef;
|
|
18
20
|
exports.useHasCommittedRef = require_useHasCommittedRef.useHasCommittedRef;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, createTemporarilyRetainedCacheItem } from "./CacheItem.cjs";
|
|
2
2
|
import { ParentCache } from "./ParentCache.cjs";
|
|
3
3
|
import { useCachedResponsivePrecommitValue } from "./useCachedResponsivePrecommitValue.cjs";
|
|
4
|
+
import { UseDisposableArrayReturn, useDisposableArray } from "./useDisposableArray.cjs";
|
|
4
5
|
import { UNASSIGNED_STATE, UnassignedState, useUpdatableDisposableState } from "./useUpdatableDisposableState.cjs";
|
|
5
6
|
import { useDisposableState } from "./useDisposableState.cjs";
|
|
6
7
|
import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.cjs";
|
|
@@ -8,4 +9,4 @@ import { useHasCommittedRef } from "./useHasCommittedRef.cjs";
|
|
|
8
9
|
import { useLazyDisposableState } from "./useLazyDisposableState.cjs";
|
|
9
10
|
import { useUpdatableDisposableClearableState } from "./useUpdatableDisposableClearableState.cjs";
|
|
10
11
|
export * from "@isograph/disposable-types";
|
|
11
|
-
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableClearableState, useUpdatableDisposableState };
|
|
12
|
+
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, UseDisposableArrayReturn, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableArray, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableClearableState, useUpdatableDisposableState };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, createTemporarilyRetainedCacheItem } from "./CacheItem.mjs";
|
|
2
2
|
import { ParentCache } from "./ParentCache.mjs";
|
|
3
3
|
import { useCachedResponsivePrecommitValue } from "./useCachedResponsivePrecommitValue.mjs";
|
|
4
|
+
import { UseDisposableArrayReturn, useDisposableArray } from "./useDisposableArray.mjs";
|
|
4
5
|
import { UNASSIGNED_STATE, UnassignedState, useUpdatableDisposableState } from "./useUpdatableDisposableState.mjs";
|
|
5
6
|
import { useDisposableState } from "./useDisposableState.mjs";
|
|
6
7
|
import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.mjs";
|
|
@@ -8,4 +9,4 @@ import { useHasCommittedRef } from "./useHasCommittedRef.mjs";
|
|
|
8
9
|
import { useLazyDisposableState } from "./useLazyDisposableState.mjs";
|
|
9
10
|
import { useUpdatableDisposableClearableState } from "./useUpdatableDisposableClearableState.mjs";
|
|
10
11
|
export * from "@isograph/disposable-types";
|
|
11
|
-
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableClearableState, useUpdatableDisposableState };
|
|
12
|
+
export { CacheItem, CacheItemOptions, CacheItemState, InParentCacheAndNotDisposed, NotInParentCacheAndDisposed, NotInParentCacheAndNotDisposed, ParentCache, UNASSIGNED_STATE, UnassignedState, UseDisposableArrayReturn, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableArray, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableClearableState, useUpdatableDisposableState };
|
package/dist/index.mjs
CHANGED
|
@@ -4,10 +4,11 @@ import { useEffectsRerunWithoutRenderRef } from "./useEffectsRerunWithoutRender.
|
|
|
4
4
|
import { useCachedResponsivePrecommitValue } from "./useCachedResponsivePrecommitValue.mjs";
|
|
5
5
|
import { useHasCommittedRef } from "./useHasCommittedRef.mjs";
|
|
6
6
|
import { UNASSIGNED_STATE, useUpdatableDisposableState } from "./useUpdatableDisposableState.mjs";
|
|
7
|
+
import { useDisposableArray } from "./useDisposableArray.mjs";
|
|
7
8
|
import { useDisposableState } from "./useDisposableState.mjs";
|
|
8
9
|
import { useLazyDisposableState } from "./useLazyDisposableState.mjs";
|
|
9
10
|
import { useUpdatableDisposableClearableState } from "./useUpdatableDisposableClearableState.mjs";
|
|
10
11
|
|
|
11
12
|
export * from "@isograph/disposable-types"
|
|
12
13
|
|
|
13
|
-
export { CacheItem, ParentCache, UNASSIGNED_STATE, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableClearableState, useUpdatableDisposableState };
|
|
14
|
+
export { CacheItem, ParentCache, UNASSIGNED_STATE, createTemporarilyRetainedCacheItem, useCachedResponsivePrecommitValue, useDisposableArray, useDisposableState, useEffectsRerunWithoutRenderRef, useHasCommittedRef, useLazyDisposableState, useUpdatableDisposableClearableState, useUpdatableDisposableState };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const require_useUpdatableDisposableState = require('./useUpdatableDisposableState.cjs');
|
|
2
|
+
let react = require("react");
|
|
3
|
+
|
|
4
|
+
//#region src/useDisposableArray.ts
|
|
5
|
+
const NO_ENTRIES = Object.freeze([]);
|
|
6
|
+
function useDisposableArray() {
|
|
7
|
+
const { state, setState } = require_useUpdatableDisposableState.useUpdatableDisposableState();
|
|
8
|
+
const setEntries = (0, react.useCallback)((nextEntries) => {
|
|
9
|
+
setState([nextEntries, () => {
|
|
10
|
+
for (const entry of nextEntries) entry[1]();
|
|
11
|
+
}]);
|
|
12
|
+
}, [setState]);
|
|
13
|
+
return {
|
|
14
|
+
entries: state === require_useUpdatableDisposableState.UNASSIGNED_STATE ? NO_ENTRIES : state,
|
|
15
|
+
setEntries
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
exports.useDisposableArray = useDisposableArray;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ItemCleanupPair } from "@isograph/disposable-types";
|
|
2
|
+
|
|
3
|
+
//#region src/useDisposableArray.d.ts
|
|
4
|
+
type UseDisposableArrayReturn<T> = {
|
|
5
|
+
readonly entries: ReadonlyArray<ItemCleanupPair<T>>;
|
|
6
|
+
readonly setEntries: (nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => void;
|
|
7
|
+
};
|
|
8
|
+
declare function useDisposableArray<T>(): UseDisposableArrayReturn<T>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { UseDisposableArrayReturn, useDisposableArray };
|
|
11
|
+
//# sourceMappingURL=useDisposableArray.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDisposableArray.d.cts","names":[],"sources":["../src/useDisposableArray.ts"],"mappings":";;;KAOY,wBAAA;EAAA,SAGD,OAAA,EAAS,aAAA,CAAc,eAAA,CAAgB,CAAA;EAAA,SAEvC,UAAA,GAAa,WAAA,EAAa,aAAA,CAAc,eAAA,CAAgB,CAAA;AAAA;AAAA,iBAoBnD,kBAAA,GAAA,CAAA,GAAyB,wBAAA,CAAyB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ItemCleanupPair } from "@isograph/disposable-types";
|
|
2
|
+
|
|
3
|
+
//#region src/useDisposableArray.d.ts
|
|
4
|
+
type UseDisposableArrayReturn<T> = {
|
|
5
|
+
readonly entries: ReadonlyArray<ItemCleanupPair<T>>;
|
|
6
|
+
readonly setEntries: (nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => void;
|
|
7
|
+
};
|
|
8
|
+
declare function useDisposableArray<T>(): UseDisposableArrayReturn<T>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { UseDisposableArrayReturn, useDisposableArray };
|
|
11
|
+
//# sourceMappingURL=useDisposableArray.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDisposableArray.d.mts","names":[],"sources":["../src/useDisposableArray.ts"],"mappings":";;;KAOY,wBAAA;EAAA,SAGD,OAAA,EAAS,aAAA,CAAc,eAAA,CAAgB,CAAA;EAAA,SAEvC,UAAA,GAAa,WAAA,EAAa,aAAA,CAAc,eAAA,CAAgB,CAAA;AAAA;AAAA,iBAoBnD,kBAAA,GAAA,CAAA,GAAyB,wBAAA,CAAyB,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { UNASSIGNED_STATE, useUpdatableDisposableState } from "./useUpdatableDisposableState.mjs";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/useDisposableArray.ts
|
|
5
|
+
const NO_ENTRIES = Object.freeze([]);
|
|
6
|
+
function useDisposableArray() {
|
|
7
|
+
const { state, setState } = useUpdatableDisposableState();
|
|
8
|
+
const setEntries = useCallback((nextEntries) => {
|
|
9
|
+
setState([nextEntries, () => {
|
|
10
|
+
for (const entry of nextEntries) entry[1]();
|
|
11
|
+
}]);
|
|
12
|
+
}, [setState]);
|
|
13
|
+
return {
|
|
14
|
+
entries: state === UNASSIGNED_STATE ? NO_ENTRIES : state,
|
|
15
|
+
setEntries
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { useDisposableArray };
|
|
21
|
+
//# sourceMappingURL=useDisposableArray.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDisposableArray.mjs","names":[],"sources":["../src/useDisposableArray.ts"],"sourcesContent":["import type { ItemCleanupPair } from '@isograph/disposable-types';\nimport { useCallback } from 'react';\nimport {\n UNASSIGNED_STATE,\n useUpdatableDisposableState,\n} from './useUpdatableDisposableState';\n\nexport type UseDisposableArrayReturn<T> = {\n // The array this render shows. It is empty until the first setEntries call commits. The hook owns every pair in\n // it, so callers read the items and never call the cleanups.\n readonly entries: ReadonlyArray<ItemCleanupPair<T>>;\n // Replaces the array. See useDisposableArray.\n readonly setEntries: (nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => void;\n};\n\nconst NO_ENTRIES: ReadonlyArray<never> = Object.freeze([]);\n\n// useDisposableArray\n// - Holds an array of disposable items in state, on top of useUpdatableDisposableState.\n// - setEntries(nextEntries) stores nextEntries. Releasing a stored array calls the cleanup of each of its pairs once.\n// useUpdatableDisposableState releases every array that is older than the committed one after the commit, and the\n// rest on unmount, including arrays that never committed.\n// - Every pair in nextEntries was acquired by the caller for this call, and the hook owns it from then on. To keep an\n// item of entries, the caller takes a new reference to it, for example with cloneIfNotDisposed() on a\n// ReferenceCountedPointer, and passes that new pair. The caller never passes a pair of entries itself and never\n// calls a cleanup the hook owns.\n// - The caller builds nextEntries from the entries of its render. If that render is stale, because its entries were\n// released after a newer array committed, taking a new reference fails (cloneIfNotDisposed() returns null). The\n// caller then releases what it acquired and does not call setEntries.\n// - Two setEntries calls before a commit both build on the same entries, and the second replaces the first.\n// - Like useUpdatableDisposableState's setState, setEntries throws if called before the initial commit. It throws\n// before storing anything, so the caller still owns nextEntries and must release them.\nexport function useDisposableArray<T>(): UseDisposableArrayReturn<T> {\n const { state, setState } =\n useUpdatableDisposableState<ReadonlyArray<ItemCleanupPair<T>>>();\n\n const setEntries = useCallback(\n (nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => {\n setState([\n nextEntries,\n () => {\n for (const entry of nextEntries) {\n entry[1]();\n }\n },\n ]);\n },\n [setState],\n );\n\n return {\n entries: state === UNASSIGNED_STATE ? NO_ENTRIES : state,\n setEntries,\n };\n}\n"],"mappings":";;;;AAeA,MAAM,aAAmC,OAAO,OAAO,EAAE,CAAC;AAiB1D,SAAgB,qBAAqD;CACnE,MAAM,EAAE,OAAO,aACb,6BAAgE;CAElE,MAAM,aAAa,aAChB,gBAAmD;AAClD,WAAS,CACP,mBACM;AACJ,QAAK,MAAM,SAAS,YAClB,OAAM,IAAI;IAGf,CAAC;IAEJ,CAAC,SAAS,CACX;AAED,QAAO;EACL,SAAS,UAAU,mBAAmB,aAAa;EACnD;EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isograph/react-disposable-state",
|
|
3
|
-
"version": "0.0.0-main-
|
|
3
|
+
"version": "0.0.0-main-36b092bd",
|
|
4
4
|
"description": "Primitives for managing disposable state in React",
|
|
5
5
|
"homepage": "https://isograph.dev",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"author": "Isograph Labs",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@isograph/disposable-types": "0.0.0-main-
|
|
11
|
+
"@isograph/disposable-types": "0.0.0-main-36b092bd"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
14
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"@types/react-test-renderer": "^18.3.0",
|
|
20
20
|
"happy-dom": "20.4.0",
|
|
21
21
|
"react-test-renderer": "^18.2.0",
|
|
22
|
-
"typescript": "5.6.3"
|
|
22
|
+
"typescript": "5.6.3",
|
|
23
|
+
"@isograph/reference-counted-pointer": "0.0.0-main-36b092bd"
|
|
23
24
|
},
|
|
24
25
|
"repository": {
|
|
25
26
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from '@isograph/disposable-types';
|
|
|
3
3
|
export * from './CacheItem';
|
|
4
4
|
export * from './ParentCache';
|
|
5
5
|
export * from './useCachedResponsivePrecommitValue';
|
|
6
|
+
export * from './useDisposableArray';
|
|
6
7
|
export * from './useDisposableState';
|
|
7
8
|
export * from './useEffectsRerunWithoutRender';
|
|
8
9
|
export * from './useHasCommittedRef';
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { ItemCleanupPair } from '@isograph/disposable-types';
|
|
2
|
+
import {
|
|
3
|
+
createReferenceCountedPointer,
|
|
4
|
+
type ReferenceCountedPointer,
|
|
5
|
+
} from '@isograph/reference-counted-pointer';
|
|
6
|
+
import { act, render, screen } from '@testing-library/react';
|
|
7
|
+
import React, { startTransition, useLayoutEffect, useState } from 'react';
|
|
8
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
type UseDisposableArrayReturn,
|
|
11
|
+
useDisposableArray,
|
|
12
|
+
} from './useDisposableArray';
|
|
13
|
+
|
|
14
|
+
type Entry = ItemCleanupPair<ReferenceCountedPointer<string>>;
|
|
15
|
+
|
|
16
|
+
function createItem(label: string) {
|
|
17
|
+
const cleanup = vi.fn();
|
|
18
|
+
return {
|
|
19
|
+
cleanup,
|
|
20
|
+
acquire: (): Entry => createReferenceCountedPointer([label, cleanup]),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type CommittedRender = UseDisposableArrayReturn<
|
|
25
|
+
ReferenceCountedPointer<string>
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
// What a caller does: builds the next array from its render's entries by taking a new reference to each kept entry
|
|
29
|
+
// and acquiring each added item, then calls setEntries. If a kept entry's reference was already released, the render
|
|
30
|
+
// is stale, so it releases everything it acquired and does not call setEntries. Returns whether it called setEntries.
|
|
31
|
+
function keepAndAdd(
|
|
32
|
+
committedRender: CommittedRender,
|
|
33
|
+
kept: ReadonlyArray<Entry>,
|
|
34
|
+
added: ReadonlyArray<ReturnType<typeof createItem>>,
|
|
35
|
+
): boolean {
|
|
36
|
+
const addedEntries = added.map((item) => item.acquire());
|
|
37
|
+
const clones: Entry[] = [];
|
|
38
|
+
for (const entry of kept) {
|
|
39
|
+
const clone = entry[0].cloneIfNotDisposed();
|
|
40
|
+
if (clone == null) {
|
|
41
|
+
for (const acquired of [...clones, ...addedEntries]) {
|
|
42
|
+
acquired[1]();
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
clones.push(clone);
|
|
47
|
+
}
|
|
48
|
+
committedRender.setEntries([...clones, ...addedEntries]);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function labelsOf(entries: ReadonlyArray<Entry>) {
|
|
53
|
+
return entries.map((entry) => entry[0].getItemIfNotDisposed()).join(',');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Renders, under StrictMode, a component that holds a useDisposableArray of reference-counted pointers to strings and
|
|
57
|
+
// shows their labels. Every commit of a new array records the render's entries and setEntries in `renders`. After the
|
|
58
|
+
// mount, it also appends the labels to `commits` and calls `onCommit`. Both run in a layout effect, which runs before
|
|
59
|
+
// the passive effect in which useUpdatableDisposableState releases older arrays. Renders are recorded per commit,
|
|
60
|
+
// because StrictMode discards the first of the two mount renders.
|
|
61
|
+
function renderDisposableArray(onCommit: (labels: string) => void = () => {}) {
|
|
62
|
+
const renders: CommittedRender[] = [];
|
|
63
|
+
const commits: string[] = [];
|
|
64
|
+
const setMountedRef: { current: (mounted: boolean) => void } = {
|
|
65
|
+
current: () => {},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function Owner() {
|
|
69
|
+
const { entries, setEntries } =
|
|
70
|
+
useDisposableArray<ReferenceCountedPointer<string>>();
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
renders.push({ entries, setEntries });
|
|
73
|
+
commits.push(labelsOf(entries));
|
|
74
|
+
onCommit(labelsOf(entries));
|
|
75
|
+
}, [entries, setEntries]);
|
|
76
|
+
return <>{`entries: ${labelsOf(entries)}`}</>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Parent() {
|
|
80
|
+
const [mounted, setMounted] = useState(true);
|
|
81
|
+
setMountedRef.current = setMounted;
|
|
82
|
+
return mounted ? <Owner /> : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render(<Parent />, { reactStrictMode: true });
|
|
86
|
+
// StrictMode runs the mount's layout effects twice.
|
|
87
|
+
commits.length = 0;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
commits,
|
|
91
|
+
latestRender: () => renders[renders.length - 1]!,
|
|
92
|
+
setMounted: (mounted: boolean) => setMountedRef.current(mounted),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('useDisposableArray', () => {
|
|
97
|
+
test('dropping the first of three entries and adding a fourth releases the first after the new array commits', () => {
|
|
98
|
+
const a = createItem('a');
|
|
99
|
+
const b = createItem('b');
|
|
100
|
+
const c = createItem('c');
|
|
101
|
+
const d = createItem('d');
|
|
102
|
+
let aCleanupCallsAtCommit: number | null = null;
|
|
103
|
+
const owner = renderDisposableArray((labels) => {
|
|
104
|
+
if (labels === 'b,c,d') {
|
|
105
|
+
aCleanupCallsAtCommit = a.cleanup.mock.calls.length;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
keepAndAdd(owner.latestRender(), [], [a, b, c]);
|
|
111
|
+
});
|
|
112
|
+
expect(screen.getByText('entries: a,b,c')).toBeTruthy();
|
|
113
|
+
|
|
114
|
+
act(() => {
|
|
115
|
+
const committedRender = owner.latestRender();
|
|
116
|
+
keepAndAdd(committedRender, committedRender.entries.slice(-2), [d]);
|
|
117
|
+
});
|
|
118
|
+
expect(screen.getByText('entries: b,c,d')).toBeTruthy();
|
|
119
|
+
expect(aCleanupCallsAtCommit).toBe(0);
|
|
120
|
+
expect(a.cleanup).toHaveBeenCalledOnce();
|
|
121
|
+
expect(b.cleanup).not.toHaveBeenCalled();
|
|
122
|
+
expect(c.cleanup).not.toHaveBeenCalled();
|
|
123
|
+
expect(d.cleanup).not.toHaveBeenCalled();
|
|
124
|
+
|
|
125
|
+
act(() => owner.setMounted(false));
|
|
126
|
+
for (const item of [a, b, c, d]) {
|
|
127
|
+
expect(item.cleanup).toHaveBeenCalledOnce();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('of two calls before a commit, the second is rendered and the first is released once', () => {
|
|
132
|
+
const a = createItem('a');
|
|
133
|
+
const x = createItem('x');
|
|
134
|
+
const y = createItem('y');
|
|
135
|
+
const owner = renderDisposableArray();
|
|
136
|
+
act(() => {
|
|
137
|
+
keepAndAdd(owner.latestRender(), [], [a]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
const committedRender = owner.latestRender();
|
|
142
|
+
keepAndAdd(committedRender, committedRender.entries, [x]);
|
|
143
|
+
keepAndAdd(committedRender, committedRender.entries, [y]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText('entries: a,y')).toBeTruthy();
|
|
147
|
+
expect(owner.commits).not.toContain('a,x');
|
|
148
|
+
expect(x.cleanup).toHaveBeenCalledOnce();
|
|
149
|
+
expect(a.cleanup).not.toHaveBeenCalled();
|
|
150
|
+
expect(y.cleanup).not.toHaveBeenCalled();
|
|
151
|
+
|
|
152
|
+
act(() => owner.setMounted(false));
|
|
153
|
+
for (const item of [a, x, y]) {
|
|
154
|
+
expect(item.cleanup).toHaveBeenCalledOnce();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('a caller from an older committed render finds its clones fail and stores nothing', () => {
|
|
159
|
+
const a = createItem('a');
|
|
160
|
+
const b = createItem('b');
|
|
161
|
+
const c = createItem('c');
|
|
162
|
+
const owner = renderDisposableArray();
|
|
163
|
+
act(() => {
|
|
164
|
+
keepAndAdd(owner.latestRender(), [], [a]);
|
|
165
|
+
});
|
|
166
|
+
const olderRender = owner.latestRender();
|
|
167
|
+
act(() => {
|
|
168
|
+
keepAndAdd(olderRender, olderRender.entries, [b]);
|
|
169
|
+
});
|
|
170
|
+
expect(screen.getByText('entries: a,b')).toBeTruthy();
|
|
171
|
+
|
|
172
|
+
// The older render's array was released after 'a,b' committed. Item a is still held by the newer array, but the
|
|
173
|
+
// older render's reference to it is released, so cloning it fails.
|
|
174
|
+
let calledSetEntries: boolean | null = null;
|
|
175
|
+
act(() => {
|
|
176
|
+
calledSetEntries = keepAndAdd(olderRender, olderRender.entries, [c]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(calledSetEntries).toBe(false);
|
|
180
|
+
expect(owner.commits).toEqual(['a', 'a,b']);
|
|
181
|
+
expect(screen.getByText('entries: a,b')).toBeTruthy();
|
|
182
|
+
expect(c.cleanup).toHaveBeenCalledOnce();
|
|
183
|
+
expect(a.cleanup).not.toHaveBeenCalled();
|
|
184
|
+
expect(b.cleanup).not.toHaveBeenCalled();
|
|
185
|
+
|
|
186
|
+
act(() => owner.setMounted(false));
|
|
187
|
+
for (const item of [a, b, c]) {
|
|
188
|
+
expect(item.cleanup).toHaveBeenCalledOnce();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('an urgent call and a transition call from the same committed array commit in turn and release every item once', () => {
|
|
193
|
+
const a = createItem('a');
|
|
194
|
+
const b = createItem('b');
|
|
195
|
+
const c = createItem('c');
|
|
196
|
+
const d = createItem('d');
|
|
197
|
+
const owner = renderDisposableArray();
|
|
198
|
+
act(() => {
|
|
199
|
+
keepAndAdd(owner.latestRender(), [], [a, b]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
act(() => {
|
|
203
|
+
const committedRender = owner.latestRender();
|
|
204
|
+
keepAndAdd(committedRender, committedRender.entries.slice(0, 1), [c]);
|
|
205
|
+
startTransition(() => {
|
|
206
|
+
keepAndAdd(committedRender, committedRender.entries, [d]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(owner.commits).toEqual(['a,b', 'a,c', 'a,b,d']);
|
|
211
|
+
expect(screen.getByText('entries: a,b,d')).toBeTruthy();
|
|
212
|
+
expect(c.cleanup).toHaveBeenCalledOnce();
|
|
213
|
+
expect(a.cleanup).not.toHaveBeenCalled();
|
|
214
|
+
expect(b.cleanup).not.toHaveBeenCalled();
|
|
215
|
+
expect(d.cleanup).not.toHaveBeenCalled();
|
|
216
|
+
|
|
217
|
+
act(() => owner.setMounted(false));
|
|
218
|
+
for (const item of [a, b, c, d]) {
|
|
219
|
+
expect(item.cleanup).toHaveBeenCalledOnce();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('unmount releases every entry once, including an array that never committed', () => {
|
|
224
|
+
const a = createItem('a');
|
|
225
|
+
const b = createItem('b');
|
|
226
|
+
const c = createItem('c');
|
|
227
|
+
const e = createItem('e');
|
|
228
|
+
const owner = renderDisposableArray();
|
|
229
|
+
act(() => {
|
|
230
|
+
keepAndAdd(owner.latestRender(), [], [a, b]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
act(() => {
|
|
234
|
+
const committedRender = owner.latestRender();
|
|
235
|
+
keepAndAdd(committedRender, committedRender.entries, [c]);
|
|
236
|
+
owner.setMounted(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(owner.commits).toEqual(['a,b']);
|
|
240
|
+
for (const item of [a, b, c]) {
|
|
241
|
+
expect(item.cleanup).toHaveBeenCalledOnce();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// After unmount, the last render's entries are released, so the caller cannot keep them. It releases the item it
|
|
245
|
+
// acquired and does not call setEntries.
|
|
246
|
+
let calledSetEntries: boolean | null = null;
|
|
247
|
+
act(() => {
|
|
248
|
+
const committedRender = owner.latestRender();
|
|
249
|
+
calledSetEntries = keepAndAdd(committedRender, committedRender.entries, [
|
|
250
|
+
e,
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
expect(calledSetEntries).toBe(false);
|
|
254
|
+
expect(e.cleanup).toHaveBeenCalledOnce();
|
|
255
|
+
for (const item of [a, b, c]) {
|
|
256
|
+
expect(item.cleanup).toHaveBeenCalledOnce();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ItemCleanupPair } from '@isograph/disposable-types';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
UNASSIGNED_STATE,
|
|
5
|
+
useUpdatableDisposableState,
|
|
6
|
+
} from './useUpdatableDisposableState';
|
|
7
|
+
|
|
8
|
+
export type UseDisposableArrayReturn<T> = {
|
|
9
|
+
// The array this render shows. It is empty until the first setEntries call commits. The hook owns every pair in
|
|
10
|
+
// it, so callers read the items and never call the cleanups.
|
|
11
|
+
readonly entries: ReadonlyArray<ItemCleanupPair<T>>;
|
|
12
|
+
// Replaces the array. See useDisposableArray.
|
|
13
|
+
readonly setEntries: (nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const NO_ENTRIES: ReadonlyArray<never> = Object.freeze([]);
|
|
17
|
+
|
|
18
|
+
// useDisposableArray
|
|
19
|
+
// - Holds an array of disposable items in state, on top of useUpdatableDisposableState.
|
|
20
|
+
// - setEntries(nextEntries) stores nextEntries. Releasing a stored array calls the cleanup of each of its pairs once.
|
|
21
|
+
// useUpdatableDisposableState releases every array that is older than the committed one after the commit, and the
|
|
22
|
+
// rest on unmount, including arrays that never committed.
|
|
23
|
+
// - Every pair in nextEntries was acquired by the caller for this call, and the hook owns it from then on. To keep an
|
|
24
|
+
// item of entries, the caller takes a new reference to it, for example with cloneIfNotDisposed() on a
|
|
25
|
+
// ReferenceCountedPointer, and passes that new pair. The caller never passes a pair of entries itself and never
|
|
26
|
+
// calls a cleanup the hook owns.
|
|
27
|
+
// - The caller builds nextEntries from the entries of its render. If that render is stale, because its entries were
|
|
28
|
+
// released after a newer array committed, taking a new reference fails (cloneIfNotDisposed() returns null). The
|
|
29
|
+
// caller then releases what it acquired and does not call setEntries.
|
|
30
|
+
// - Two setEntries calls before a commit both build on the same entries, and the second replaces the first.
|
|
31
|
+
// - Like useUpdatableDisposableState's setState, setEntries throws if called before the initial commit. It throws
|
|
32
|
+
// before storing anything, so the caller still owns nextEntries and must release them.
|
|
33
|
+
export function useDisposableArray<T>(): UseDisposableArrayReturn<T> {
|
|
34
|
+
const { state, setState } =
|
|
35
|
+
useUpdatableDisposableState<ReadonlyArray<ItemCleanupPair<T>>>();
|
|
36
|
+
|
|
37
|
+
const setEntries = useCallback(
|
|
38
|
+
(nextEntries: ReadonlyArray<ItemCleanupPair<T>>) => {
|
|
39
|
+
setState([
|
|
40
|
+
nextEntries,
|
|
41
|
+
() => {
|
|
42
|
+
for (const entry of nextEntries) {
|
|
43
|
+
entry[1]();
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
},
|
|
48
|
+
[setState],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
entries: state === UNASSIGNED_STATE ? NO_ENTRIES : state,
|
|
53
|
+
setEntries,
|
|
54
|
+
};
|
|
55
|
+
}
|