From 5152616bafb922dcf2f98bbd53064fdf908ec11d Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 20:43:41 +0300 Subject: [PATCH 1/9] chore: react-palm to kepler.gl/tasks Signed-off-by: Ihor Dykhta --- README.md | 8 +- UPGRADE-GUIDE.md | 1 + docs/api-reference/actions/actions.md | 2 +- docs/api-reference/get-started.md | 12 +- docs/api-reference/reducers/reducers.md | 2 +- docs/upgrade-guide-v3.3.md | 57 +++ examples/custom-map-style/package.json | 3 +- examples/custom-reducer/package.json | 3 +- examples/custom-theme/package.json | 3 +- examples/demo-app/src/reducers/index.js | 2 +- examples/node-app/package.json | 3 +- examples/open-modal/package.json | 3 +- examples/replace-component/package.json | 3 +- src/actions/package.json | 1 - src/actions/src/action-types.ts | 2 +- src/reducers/package.json | 1 - src/reducers/src/map-style-updaters.ts | 2 +- src/reducers/src/merger-handler.ts | 2 +- src/reducers/src/middleware.ts | 8 +- src/reducers/src/provider-state-updaters.ts | 2 +- src/reducers/src/root.ts | 2 +- src/reducers/src/vis-state-updaters.ts | 5 +- src/table/package.json | 2 +- src/table/src/dataset-utils.ts | 2 +- src/tasks/package.json | 3 +- src/tasks/src/index.ts | 21 +- src/tasks/src/task-runtime.ts | 454 ++++++++++++++++++++ test/browser/components/kepler-gl-test.js | 2 +- test/helpers/mock-state-utils.js | 2 +- test/node/reducers/composer-state-test.js | 2 +- test/node/reducers/map-style-test.js | 2 +- test/node/reducers/provider-state-test.js | 2 +- test/node/reducers/vis-state-merger-test.js | 2 +- test/node/reducers/vis-state-test.js | 2 +- website/package.json | 3 +- website/src/reducers/index.js | 2 +- 36 files changed, 574 insertions(+), 54 deletions(-) create mode 100644 src/tasks/src/task-runtime.ts diff --git a/README.md b/README.md index f55fd96b0c..34b93d5a55 100644 --- a/README.md +++ b/README.md @@ -90,15 +90,14 @@ Here are the basic steps to import kepler.gl into your app. You also take a look ### 1. Mount reducer -Kepler.gl uses Redux to manage its internal state, along with [react-palm][react-palm] middleware to handle side effects. +Kepler.gl uses Redux to manage its internal state, along with a built-in task middleware to handle async side effects. -You need to add `taskMiddleware` of `react-palm` to your store too. We are actively working on a solution where -`react-palm` will not be required, however it is still a very lightweight side effects management tool that is easier to test than react-thunk. +You need to add `taskMiddleware` to your store. The easiest way is via `enhanceReduxMiddleware` from `@kepler.gl/reducers`: ```js import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; import keplerGlReducer from '@kepler.gl/reducers'; -import {enhanceReduxMiddleware} from '@kepler.gl/middleware'; +import {enhanceReduxMiddleware} from '@kepler.gl/reducers'; const initialState = {}; const reducers = combineReducers({ @@ -525,7 +524,6 @@ Read more about [addDataToMap](./docs/api-reference/actions/actions.md#adddatato [mapbox-token]: https://www.mapbox.com/help/define-access-token/ [developers]: contributing/DEVELOPERS.md [examples]: https://github.com/keplergl/kepler.gl/tree/master/examples -[react-palm]: https://github.com/btford/react-palm [roadmap]: https://github.com/keplergl/kepler.gl/wiki/Kepler.gl-2019-Roadmap [stack]: https://stackoverflow.com/questions/tagged/kepler.gl [web]: http://www.kepler.gl/ diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md index 43b3f41c9f..f9c10f5c5f 100644 --- a/UPGRADE-GUIDE.md +++ b/UPGRADE-GUIDE.md @@ -27,6 +27,7 @@ See the full upgrade guide: **[Upgrade Guide — kepler.gl 3.3](docs/upgrade-gui - **HeatmapLayer** — rewritten from Mapbox GL to deck.gl base - **`layerOrder`** — type changed from flat `string[]` to `(string | LayerOrderGroup)[]` - **`LayerSelectorPanelFactory`** — removed from `@kepler.gl/components` +- **`react-palm` removed** — replace `import {taskMiddleware} from 'react-palm/tasks'` with `import {taskMiddleware} from '@kepler.gl/reducers'` ## Upgrade from v2.4 to v3.0 diff --git a/docs/api-reference/actions/actions.md b/docs/api-reference/actions/actions.md index 60a1a94fe7..e6e2e85bc4 100644 --- a/docs/api-reference/actions/actions.md +++ b/docs/api-reference/actions/actions.md @@ -202,7 +202,7 @@ Type: [Object][164] // store.js import {handleActions} from 'redux-actions'; import {createStore, combineReducers, applyMiddleware} from 'redux'; -import {taskMiddleware} from 'react-palm/tasks'; +import {taskMiddleware} from '@kepler.gl/tasks'; import keplerGlReducer from '@kepler.gl/reducers'; import {ActionTypes} from '@kepler.gl/actions'; diff --git a/docs/api-reference/get-started.md b/docs/api-reference/get-started.md index a8e993577e..205a242758 100644 --- a/docs/api-reference/get-started.md +++ b/docs/api-reference/get-started.md @@ -91,12 +91,11 @@ export default Root; #### 1. Mount reducer -Kepler.gl uses [Redux](https://redux.js.org/) to manage its internal state, along with [react-palm](https://github.com/btford/react-palm) middleware to handle side effects. Mount kepler.gl reducer in your store, apply `taskMiddleware`. +Kepler.gl uses [Redux](https://redux.js.org/) to manage its internal state, along with a built-in task middleware to handle async side effects. Mount kepler.gl reducer in your store and apply the middleware using `enhanceReduxMiddleware`: ```js -import keplerGlReducer from '@kepler.gl/reducers'; +import keplerGlReducer, {enhanceReduxMiddleware} from '@kepler.gl/reducers'; import {createStore, combineReducers, applyMiddleware} from 'redux'; -import {taskMiddleware} from 'react-palm/tasks'; const reducer = combineReducers({ // <-- mount kepler.gl reducer in your app @@ -106,9 +105,14 @@ const reducer = combineReducers({ app: appReducer }); +const middlewares = enhanceReduxMiddleware([ + // Add other middlewares here +]); + // create store -const store = createStore(reducer, {}, applyMiddleware(taskMiddleware)); +const store = createStore(reducer, {}, applyMiddleware(...middlewares)); ``` + If you mount `keplerGlReducer` in another address instead of `keplerGl`, or it is not mounted at root of your reducer, you will need to specify the path to it when you mount the component with the `getState` prop. diff --git a/docs/api-reference/reducers/reducers.md b/docs/api-reference/reducers/reducers.md index f7b1c9893f..9bbc5003ec 100644 --- a/docs/api-reference/reducers/reducers.md +++ b/docs/api-reference/reducers/reducers.md @@ -21,7 +21,7 @@ to mount it at another address e.g. `foo` you will need to specify it when you m ```javascript import keplerGlReducer from '@kepler.gl/reducers'; import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; -import {taskMiddleware} from 'react-palm/tasks'; +import {taskMiddleware} from '@kepler.gl/tasks'; const initialState = {}; const reducers = combineReducers({ diff --git a/docs/upgrade-guide-v3.3.md b/docs/upgrade-guide-v3.3.md index 292a6f08cb..cd871dc9c6 100644 --- a/docs/upgrade-guide-v3.3.md +++ b/docs/upgrade-guide-v3.3.md @@ -37,6 +37,7 @@ kepler.gl 3.3 upgrades the rendering stack from **deck.gl 8 / luma.gl 8** to **d | ----------------------------------- | ------------------------------------------------ | | `hubble.gl/core`, `hubble.gl/react` | Removed from kepler.gl | | `@nebula.gl/layers` | Replaced by `@deck.gl-community/editable-layers` | +| `react-palm` | Replaced by built-in task runtime in `@kepler.gl/tasks` | ### Yarn resolutions @@ -44,6 +45,62 @@ All `@deck.gl/*`, `@loaders.gl/*`, and `@luma.gl/*` packages are pinned via reso --- +## Breaking Changes — `react-palm` removed + +kepler.gl no longer depends on `react-palm`. The task middleware that handles async side effects is now built into `@kepler.gl/tasks`. + +### Store setup migration + +**Before (v3.2 and earlier):** + +```js +import {taskMiddleware} from 'react-palm/tasks'; +import {createStore, applyMiddleware} from 'redux'; +import keplerGlReducer from '@kepler.gl/reducers'; + +const store = createStore(reducer, {}, applyMiddleware(taskMiddleware)); +``` + +**After (v3.3+) — recommended:** + +```js +import keplerGlReducer, {enhanceReduxMiddleware} from '@kepler.gl/reducers'; +import {createStore, applyMiddleware} from 'redux'; + +const store = createStore(reducer, {}, applyMiddleware(...enhanceReduxMiddleware())); +``` + +**After (v3.3+) — if you prefer importing `taskMiddleware` directly:** + +```js +import {taskMiddleware} from '@kepler.gl/tasks'; +import {createStore, applyMiddleware} from 'redux'; + +const store = createStore(reducer, {}, applyMiddleware(taskMiddleware)); +``` + +### Why this matters + +If you continue to import `taskMiddleware` from `react-palm/tasks` after upgrading, kepler.gl's internal async operations (file loading, map style fetching, cloud save/load) will **silently stop working** — the middleware will watch the wrong task queue. There is no runtime error; tasks simply never complete. + +### What to do + +1. Remove `react-palm` from your `package.json` dependencies. +2. Replace any `import {taskMiddleware} from 'react-palm/tasks'` with `import {taskMiddleware} from '@kepler.gl/tasks'` — or switch to `enhanceReduxMiddleware` from `@kepler.gl/reducers` (recommended). +3. Run `npm install` / `yarn install` to remove the `react-palm` package. + +### `enhanceReduxMiddleware` is unchanged + +The `enhanceReduxMiddleware` helper from `@kepler.gl/reducers` — the recommended way to set up the store — continues to work identically. If your app already uses `enhanceReduxMiddleware`, **no migration is needed**. + +```js +// This pattern requires zero changes +import keplerGlReducer, {enhanceReduxMiddleware} from '@kepler.gl/reducers'; +const middlewares = enhanceReduxMiddleware([thunk, myOtherMiddleware]); +``` + +--- + ## Breaking Changes — React 19 kepler.gl 3.3 requires **React 19**. React 18 is no longer supported. diff --git a/examples/custom-map-style/package.json b/examples/custom-map-style/package.json index ac5640bf55..1d5c2a6dec 100644 --- a/examples/custom-map-style/package.json +++ b/examples/custom-map-style/package.json @@ -10,7 +10,6 @@ "global": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-palm": "^3.3.6", "react-redux": "^8.0.5", "react-virtualized": "^9.21.0", "redux-actions": "^2.2.1", @@ -22,4 +21,4 @@ "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-replace": "^1.4.0" } -} +} \ No newline at end of file diff --git a/examples/custom-reducer/package.json b/examples/custom-reducer/package.json index d9c4921c8f..c771fa27da 100644 --- a/examples/custom-reducer/package.json +++ b/examples/custom-reducer/package.json @@ -11,7 +11,6 @@ "global": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-palm": "^3.3.6", "react-redux": "^8.0.5", "react-virtualized": "^9.21.0", "redux-actions": "^2.2.1", @@ -23,4 +22,4 @@ "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-replace": "^1.4.0" } -} +} \ No newline at end of file diff --git a/examples/custom-theme/package.json b/examples/custom-theme/package.json index ac5640bf55..1d5c2a6dec 100644 --- a/examples/custom-theme/package.json +++ b/examples/custom-theme/package.json @@ -10,7 +10,6 @@ "global": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-palm": "^3.3.6", "react-redux": "^8.0.5", "react-virtualized": "^9.21.0", "redux-actions": "^2.2.1", @@ -22,4 +21,4 @@ "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-replace": "^1.4.0" } -} +} \ No newline at end of file diff --git a/examples/demo-app/src/reducers/index.js b/examples/demo-app/src/reducers/index.js index a11b4d7698..74a40f86cd 100644 --- a/examples/demo-app/src/reducers/index.js +++ b/examples/demo-app/src/reducers/index.js @@ -3,7 +3,7 @@ import {combineReducers} from 'redux'; import {handleActions} from 'redux-actions'; -import Task, {withTask} from 'react-palm/tasks'; +import Task, {withTask} from '@kepler.gl/tasks'; import {aiAssistantReducer} from '@kepler.gl/ai-assistant'; import {EXPORT_MAP_FORMATS} from '@kepler.gl/constants'; diff --git a/examples/node-app/package.json b/examples/node-app/package.json index fd6bf27cd2..8d136be112 100644 --- a/examples/node-app/package.json +++ b/examples/node-app/package.json @@ -11,7 +11,6 @@ "global": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-palm": "^3.3.6", "react-redux": "^8.0.5", "react-virtualized": "^9.21.0", "redux-actions": "^2.2.1", @@ -23,4 +22,4 @@ "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-replace": "^1.4.0" } -} +} \ No newline at end of file diff --git a/examples/open-modal/package.json b/examples/open-modal/package.json index e2db0caa23..69d56889cd 100644 --- a/examples/open-modal/package.json +++ b/examples/open-modal/package.json @@ -11,7 +11,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-modal": "^3.1.10", - "react-palm": "^3.3.6", "react-redux": "^8.0.5", "react-virtualized": "^9.21.0", "redux-actions": "^2.2.1", @@ -23,4 +22,4 @@ "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-replace": "^1.4.0" } -} +} \ No newline at end of file diff --git a/examples/replace-component/package.json b/examples/replace-component/package.json index 6e69457e2a..5736e82052 100644 --- a/examples/replace-component/package.json +++ b/examples/replace-component/package.json @@ -14,7 +14,6 @@ "global": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-palm": "^3.3.6", "react-redux": "^8.0.5", "react-virtualized": "^9.21.0", "redux-actions": "^2.2.1", @@ -33,4 +32,4 @@ "yarn": "4.4.0" }, "packageManager": "yarn@4.4.0" -} +} \ No newline at end of file diff --git a/src/actions/package.json b/src/actions/package.json index 1e8b0e1647..134563c94c 100644 --- a/src/actions/package.json +++ b/src/actions/package.json @@ -41,7 +41,6 @@ "@types/react-redux": "^7.1.23", "@types/redux-actions": "^2.6.2", "lodash": "4.17.23", - "react-palm": "^3.3.8", "react-redux": "^8.0.5", "redux": "^5.0.0", "redux-actions": "^2.2.1" diff --git a/src/actions/src/action-types.ts b/src/actions/src/action-types.ts index fd033a1aa3..ea72543881 100644 --- a/src/actions/src/action-types.ts +++ b/src/actions/src/action-types.ts @@ -13,7 +13,7 @@ export const ACTION_PREFIX = '@@kepler.gl/'; * // store.js * import {handleActions} from 'redux-actions'; * import {createStore, combineReducers, applyMiddleware} from 'redux'; - * import {taskMiddleware} from 'react-palm/tasks'; + * import {taskMiddleware} from '@kepler.gl/reducers'; * * import keplerGlReducer from '@kepler.gl/reducers'; * import {ActionTypes} from '@kepler.gl/actions'; diff --git a/src/reducers/package.json b/src/reducers/package.json index 65026fef20..677bb84ad6 100644 --- a/src/reducers/package.json +++ b/src/reducers/package.json @@ -57,7 +57,6 @@ "deepmerge": "^4.2.2", "global": "^4.3.0", "lodash": "4.17.23", - "react-palm": "^3.3.8", "redux": "^5.0.0", "redux-actions": "^2.2.1", "reselect": "^5.0.0" diff --git a/src/reducers/src/map-style-updaters.ts b/src/reducers/src/map-style-updaters.ts index b7d7b1f768..fdb4971bc2 100644 --- a/src/reducers/src/map-style-updaters.ts +++ b/src/reducers/src/map-style-updaters.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import Task, {withTask} from 'react-palm/tasks'; +import Task, {withTask} from '@kepler.gl/tasks'; import cloneDeep from 'lodash/cloneDeep'; import Console from 'global/console'; diff --git a/src/reducers/src/merger-handler.ts b/src/reducers/src/merger-handler.ts index 86b2e28d2e..68d52f6d06 100644 --- a/src/reducers/src/merger-handler.ts +++ b/src/reducers/src/merger-handler.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import {getGlobalTaskQueue} from 'react-palm/tasks'; +import {getGlobalTaskQueue} from '@kepler.gl/tasks'; import {isObject} from '@kepler.gl/utils'; import {toArray} from '@kepler.gl/common-utils'; import {ValueOf} from '@kepler.gl/types'; diff --git a/src/reducers/src/middleware.ts b/src/reducers/src/middleware.ts index e6ec5f66c6..fb2446fe53 100644 --- a/src/reducers/src/middleware.ts +++ b/src/reducers/src/middleware.ts @@ -2,16 +2,14 @@ // Copyright contributors to the kepler.gl project // Extra helpers for redux -// We are exposing this secause react-palm has no UMD module and -// users need taskMiddleware to initiate their redux middle ware -import {taskMiddleware} from 'react-palm/tasks'; +import {taskMiddleware} from '@kepler.gl/tasks'; import {Middleware} from 'redux'; /** * This method is used to enhance redux middleware and provide - * functionality to support react-palm + * functionality to support the kepler.gl task system * @param middlewares current redux middlewares - * @returns {*[]} the original list of middlewares plus the react-palm middleware + * @returns {*[]} the original list of middlewares plus the task middleware */ export function enhanceReduxMiddleware(middlewares: Middleware[] = []): Middleware[] { return [...middlewares, taskMiddleware]; diff --git a/src/reducers/src/provider-state-updaters.ts b/src/reducers/src/provider-state-updaters.ts index b4cedeb20c..36c2f2eb71 100644 --- a/src/reducers/src/provider-state-updaters.ts +++ b/src/reducers/src/provider-state-updaters.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import Task, {withTask} from 'react-palm/tasks'; +import Task, {withTask} from '@kepler.gl/tasks'; import Console from 'global/console'; import {getApplicationConfig, getError, isPlainObject} from '@kepler.gl/utils'; import {generateHashId, toArray} from '@kepler.gl/common-utils'; diff --git a/src/reducers/src/root.ts b/src/reducers/src/root.ts index 0ae183f266..7237ec3d17 100644 --- a/src/reducers/src/root.ts +++ b/src/reducers/src/root.ts @@ -231,7 +231,7 @@ function decorate(target, savedInitialState = {}) { * @example * import keplerGlReducer from '@kepler.gl/reducers'; * import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; - * import {taskMiddleware} from 'react-palm/tasks'; + * import {taskMiddleware} from '@kepler.gl/reducers'; * * const initialState = {}; * const reducers = combineReducers({ diff --git a/src/reducers/src/vis-state-updaters.ts b/src/reducers/src/vis-state-updaters.ts index ea729d2286..3b606965c2 100644 --- a/src/reducers/src/vis-state-updaters.ts +++ b/src/reducers/src/vis-state-updaters.ts @@ -12,7 +12,7 @@ import isEqual from 'lodash/isEqual'; import pick from 'lodash/pick'; import uniq from 'lodash/uniq'; import xor from 'lodash/xor'; -import Task, {disableStackCapturing, withTask} from 'react-palm/tasks'; +import Task, {disableStackCapturing, withTask} from '@kepler.gl/tasks'; // Tasks import { DELAY_TASK, @@ -190,8 +190,7 @@ import {PayloadAction} from '@reduxjs/toolkit'; import {findMapBounds} from './data-utils'; -// react-palm -// disable capture exception for react-palm call to withTask +// disable stack capture for withTask calls that happen outside strict reducer context disableStackCapturing(); /** diff --git a/src/table/package.json b/src/table/package.json index b4270f9034..8ee8907598 100644 --- a/src/table/package.json +++ b/src/table/package.json @@ -30,6 +30,7 @@ "dependencies": { "@kepler.gl/common-utils": "3.3.0-alpha.4", "@kepler.gl/constants": "3.3.0-alpha.4", + "@kepler.gl/tasks": "3.3.0-alpha.4", "@kepler.gl/types": "3.3.0-alpha.4", "@kepler.gl/utils": "3.3.0-alpha.4", "@loaders.gl/mvt": "^4.4.1", @@ -40,7 +41,6 @@ "global": "^4.3.0", "lodash": "4.17.23", "moment": "^2.10.6", - "react-palm": "^3.3.8", "type-analyzer": "0.4.0" }, "nyc": { diff --git a/src/table/src/dataset-utils.ts b/src/table/src/dataset-utils.ts index b317f71513..f5e7c20e12 100644 --- a/src/table/src/dataset-utils.ts +++ b/src/table/src/dataset-utils.ts @@ -4,7 +4,7 @@ import uniq from 'lodash/uniq'; import KeplerTable, {Datasets} from './kepler-table'; import {ProtoDataset, RGBColor} from '@kepler.gl/types'; -import Task from 'react-palm/tasks'; +import Task from '@kepler.gl/tasks'; import { DatasetType, diff --git a/src/tasks/package.json b/src/tasks/package.json index 08b069cdef..8ad0721df8 100644 --- a/src/tasks/package.json +++ b/src/tasks/package.json @@ -28,8 +28,7 @@ "dist" ], "dependencies": { - "@kepler.gl/processors": "3.3.0-alpha.4", - "react-palm": "^3.3.8" + "@kepler.gl/processors": "3.3.0-alpha.4" }, "nyc": { "sourceMap": false, diff --git a/src/tasks/src/index.ts b/src/tasks/src/index.ts index 9fbe5c9334..8c77f03650 100644 --- a/src/tasks/src/index.ts +++ b/src/tasks/src/index.ts @@ -1,9 +1,28 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import Task, {taskCreator} from 'react-palm/tasks'; +import Task, {taskCreator} from './task-runtime'; import {readFileInBatches, processFileData} from '@kepler.gl/processors'; +export { + Task, + taskCreator, + taskMiddleware, + withTask, + withTasks, + getGlobalTaskQueue, + disableStackCapturing, + drainTasksForTesting, + succeedTaskInTest, + errorTaskInTest, + succeedTaskWithValues, + simulateTask, + reportTasksForTesting, + all, + allSettled +} from './task-runtime'; +export type {TaskDescriptor} from './task-runtime'; + export const LOAD_FILE_TASK = Task.fromPromise( ({file, fileCache, loaders, loadOptions}) => readFileInBatches({file, fileCache, loaders, loadOptions}), diff --git a/src/tasks/src/task-runtime.ts b/src/tasks/src/task-runtime.ts new file mode 100644 index 0000000000..01182008f3 --- /dev/null +++ b/src/tasks/src/task-runtime.ts @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +/** + * Lightweight framework-neutral task runtime for kepler.gl reducers. + * + * Tasks are plain descriptors that encode async side effects. They are + * collected by `withTask()` inside reducer updaters and drained by + * `taskMiddleware` after each Redux dispatch, keeping reducers pure. + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type ResolveFn = (value: any) => any; +type RejectFn = (reason: any) => any; +type ProgressCtx = {onProgress: ResolveFn}; + +/** + * The function that actually performs an effect when a task is executed. + * Called by the middleware with resolve/reject continuations. + */ +type EffectFn = ( + resolve: ResolveFn, + reject: RejectFn, + ctx?: ProgressCtx +) => any; + +type RunnerFn = ( + effect: EffectFn & {payload?: any; label?: string}, + resolve: ResolveFn, + reject: RejectFn, + ctx?: ProgressCtx +) => any; + +export type TaskDescriptor = { + /** Human-readable name used in error messages and devtools */ + label: string; + /** Mirrors the `type` field — kept for backwards compatibility with existing task consumers */ + type: string; + payload: any; + run: (runner: RunnerFn, resolve: ResolveFn, reject: RejectFn, ctx?: ProgressCtx) => any; + /** Transform the success value */ + map: (fn: ResolveFn) => TaskDescriptor; + /** Transform both success and failure branches */ + bimap: (onResolve: ResolveFn, onReject: RejectFn) => TaskDescriptor; + /** Chain another task on success */ + chain: (fn: (value: any) => TaskDescriptor) => TaskDescriptor; +}; + +/** A factory function that creates a TaskDescriptor from an argument */ +export type TaskFactory = ((arg: any) => TaskDescriptor) & {label?: string; type?: string}; + +type InstrumentHook = ( + event: 'start' | 'success' | 'error', + task: TaskDescriptor, + value: any +) => void; + +// --------------------------------------------------------------------------- +// Instrumentation (test hook) +// --------------------------------------------------------------------------- + +let instrumentHook: InstrumentHook = () => {}; + +/** + * Override the instrumentation hook — used in tests to observe task + * lifecycle events without running the middleware. + */ +export function reportTasksForTesting(hook: InstrumentHook): void { + instrumentHook = hook; +} + +// --------------------------------------------------------------------------- +// Core task construction +// --------------------------------------------------------------------------- + +function buildTask(payload: any, runFn: TaskDescriptor['run'], label: string): TaskDescriptor { + const descriptor: TaskDescriptor = { + label, + type: label, + payload, + run: runFn, + map(transform) { + return buildTask( + payload, + (runner, resolve, reject, ctx) => + runFn(runner, value => resolve(transform(value)), reject, ctx), + label + ); + }, + bimap(onResolve, onReject) { + return buildTask( + payload, + (runner, resolve, reject, ctx) => + runFn( + runner, + value => resolve(onResolve(value)), + reason => reject(onReject(reason)), + ctx + ), + label + ); + }, + chain(next) { + return buildTask( + payload, + (runner, resolve, reject, ctx) => + runFn( + runner, + value => next(value).run(runner, resolve, reject, ctx), + reject, + ctx + ), + `Chain(${label})` + ); + } + }; + return descriptor; +} + +/** + * Wrap an EffectFn so calls are routed through the instrumentation hook. + */ +function wrapWithInstrumentation( + effect: EffectFn, + payload: any, + label: string +): TaskDescriptor { + let self: TaskDescriptor; + const traced = Object.assign( + (resolve: ResolveFn, reject: RejectFn, ctx?: ProgressCtx) => { + instrumentHook('start', self, payload); + return effect( + value => { + instrumentHook('success', self, value); + return resolve(value); + }, + reason => { + instrumentHook('error', self, reason); + return reject(reason); + }, + ctx + ); + }, + {payload, label} + ); + self = buildTask( + payload, + (runner, resolve, reject, ctx) => runner(traced, resolve, reject, ctx), + label + ); + return self; +} + +// --------------------------------------------------------------------------- +// Public task factory helpers +// --------------------------------------------------------------------------- + +/** + * Create a task factory from a function that returns a Promise. + * + * @example + * const FETCH_DATA = fromPromise(url => fetch(url).then(r => r.json()), 'FETCH_DATA'); + * // in a reducer: + * return withTask(newState, FETCH_DATA(url).bimap(onSuccess, onError)); + */ +export function fromPromise(fn: (arg: any) => Promise, label: string): TaskFactory { + return Object.assign( + (arg: any) => + wrapWithInstrumentation( + (resolve, reject) => fn(arg).then(resolve, reject), + arg, + label + ), + {type: label, label} + ); +} + +/** + * Create a task factory from a Node-style callback function. + * The callback receives `(error, result)`. + */ +export function fromCallback( + fn: (arg: any, done: (err: any, result?: any) => void) => any, + label: string +): TaskFactory { + return Object.assign( + (arg: any) => + wrapWithInstrumentation( + (resolve, reject) => fn(arg, (err, result) => (err ? reject(err) : resolve(result))), + arg, + label + ), + {type: label, label} + ); +} + +/** + * Create a task factory from a function that accepts explicit + * `(arg, resolve, reject)` parameters. + */ +export function taskCreator(fn: Function, label: string): TaskFactory { + return Object.assign( + (arg: any) => + wrapWithInstrumentation( + (resolve, reject) => fn(arg, resolve, reject), + arg, + label + ), + {type: label, label} + ); +} + +// --------------------------------------------------------------------------- +// Parallel task combinators +// --------------------------------------------------------------------------- + +function combineParallel(tasks: TaskDescriptor[], settled: boolean): TaskDescriptor { + const label = `Task.${settled ? 'allSettled' : 'all'}(${tasks.map(t => t.label).join(', ')})`; + return buildTask( + tasks.map(t => t.payload), + (runner, resolve, reject, ctx) => { + if (tasks.length === 0) { + return resolve([]); + } + const results = new Array(tasks.length); + let remaining = tasks.length; + let aborted = false; + + const onResolved = (idx: number) => (value: any) => { + if (aborted) return; + results[idx] = settled ? {status: 'fulfilled', value} : value; + remaining -= 1; + if (remaining === 0) resolve(results); + }; + + const onRejected = (idx: number) => (reason: any) => { + if (aborted) return; + if (!settled) { + aborted = true; + return reject(reason); + } + results[idx] = {status: 'rejected', value: reason}; + remaining -= 1; + if (remaining === 0) resolve(results); + }; + + return Promise.allSettled( + tasks.map((task, i) => + task.run(runner, onResolved(i), onRejected(i), ctx) + ) + ); + }, + label + ); +} + +/** Run all tasks in parallel; reject as soon as any one fails */ +export const all = (tasks: TaskDescriptor[]): TaskDescriptor => combineParallel(tasks, false); + +/** Run all tasks in parallel; always resolve with `{status, value}` records */ +export const allSettled = (tasks: TaskDescriptor[]): TaskDescriptor => + combineParallel(tasks, true); + +// --------------------------------------------------------------------------- +// Global task queue +// --------------------------------------------------------------------------- + +// Stored on globalThis so the same queue is shared even when multiple +// bundled copies of this module coexist (e.g. app + library both bundle kepler). +const QUEUE_KEY = '___KEPLER_TASK_QUEUE_3a4f9c1d'; +const g = globalThis as typeof globalThis & Record; +g[QUEUE_KEY] ??= {pending: [] as TaskDescriptor[], lastOrigin: null as Error | null}; + +const getQueue = (): TaskDescriptor[] => g[QUEUE_KEY].pending; +const setQueue = (tasks: TaskDescriptor[]) => { + g[QUEUE_KEY].pending = tasks; +}; + +// --------------------------------------------------------------------------- +// Stack capture (dev ergonomic — helps locate misplaced withTask() calls) +// --------------------------------------------------------------------------- + +let captureEnabled = true; + +/** + * Disable the stack-trace guard that detects `withTask()` calls outside + * of reducers. Safe to call in production builds for a minor perf gain. + */ +export const disableStackCapturing = (): void => { + captureEnabled = false; +}; + +// --------------------------------------------------------------------------- +// withTask — attach tasks to reducer state +// --------------------------------------------------------------------------- + +/** + * Attach one or more tasks to a reducer state snapshot. + * The tasks are queued and executed by `taskMiddleware` after the dispatch + * completes, keeping the reducer itself synchronous. + * + * @example + * function myUpdater(state, action) { + * const nextState = { ...state, loading: true }; + * const task = LOAD_DATA(action.payload).bimap(loadSuccess, loadError); + * return withTask(nextState, task); + * } + */ +export function withTask(state: S, tasks: TaskDescriptor | TaskDescriptor[]): S { + if (captureEnabled && !g[QUEUE_KEY].lastOrigin) { + g[QUEUE_KEY].lastOrigin = new Error( + '[kepler.gl] withTask() was called outside of a reducer. ' + + 'Tasks must be attached inside reducer updater functions.' + ); + } + const incoming = Array.isArray(tasks) ? tasks : [tasks]; + setQueue(getQueue().concat(incoming)); + return state; +} + +/** Alias — accepts an array of tasks directly (same as withTask with an array) */ +export const withTasks = withTask; + +// --------------------------------------------------------------------------- +// Redux middleware +// --------------------------------------------------------------------------- + +const microtask = Promise.resolve(); + +/** + * Redux middleware that drains the task queue after every dispatched action. + * Add this to your store's middleware chain. + * + * @example + * import { taskMiddleware } from '@kepler.gl/tasks'; + * const store = createStore(reducer, applyMiddleware(taskMiddleware)); + */ +export const taskMiddleware = + (store: any) => (next: any) => (action: any): any => { + if (captureEnabled && getQueue().length) { + const err = g[QUEUE_KEY].lastOrigin; + g[QUEUE_KEY].lastOrigin = null; + throw err; + } + + next(action); + + const tasks = getQueue(); + if (tasks.length === 0) return microtask; + + setQueue([]); + g[QUEUE_KEY].lastOrigin = null; + + // Dispatch task results asynchronously to avoid re-entrancy issues. + const dispatch = (nextAction: any) => + microtask.then(() => store.dispatch(nextAction)); + + const runEffect: RunnerFn = (effect, resolve, reject, ctx) => + effect(resolve, reject, ctx); + + return Promise.all( + tasks.map(task => + task.run(runEffect, dispatch, dispatch, {onProgress: dispatch}) + ) + ); + }; + +// --------------------------------------------------------------------------- +// Test utilities +// --------------------------------------------------------------------------- + +/** + * @internal + * Execute a task synchronously using a simulator function. + * Throws if neither the resolve nor reject branch is called. + */ +function runSync(task: TaskDescriptor, simulator: RunnerFn): any { + let outcome: any; + let settled = false; + task.run( + simulator, + value => { + outcome = value; + settled = true; + }, + reason => { + outcome = reason; + settled = true; + } + ); + if (!settled) { + throw new Error( + `[kepler.gl] Task "${task.label}" did not call resolve or reject synchronously. ` + + 'Use drainTasksForTesting() for async tasks.' + ); + } + return outcome; +} + +/** Simulate a task succeeding with a given value */ +export const succeedTaskInTest = (task: TaskDescriptor, value: any): any => + runSync(task, (_effect, resolve) => resolve(value)); + +/** Simulate a task failing with a given reason */ +export const errorTaskInTest = (task: TaskDescriptor, reason: any): any => + runSync(task, (_effect, _resolve, reject) => reject(reason)); + +/** Simulate a task with a custom runner (for complex scenarios) */ +export const simulateTask = (task: TaskDescriptor, runner: RunnerFn): any => + runSync(task, runner); + +/** + * Simulate a task that resolves multiple times (e.g. a chained sequence), + * consuming values from the provided array in order. + */ +export const succeedTaskWithValues = (task: TaskDescriptor, values: any[]): any => { + let cursor = 0; + return runSync(task, (_effect, resolve) => { + if (cursor >= values.length) { + throw new Error( + `[kepler.gl] succeedTaskWithValues: ran out of values at index ${cursor}` + ); + } + return resolve(values[cursor++]); + }); +}; + +/** + * Drain the task queue and return all pending tasks. + * Call this in tests instead of running the full middleware. + */ +export const drainTasksForTesting = (): TaskDescriptor[] => { + const tasks = getQueue(); + setQueue([]); + g[QUEUE_KEY].lastOrigin = null; + return tasks; +}; + +/** + * Return the current task queue without clearing it. + * Used by merger-handler to detect tasks scheduled during a merge step. + */ +export const getGlobalTaskQueue = getQueue; + +// --------------------------------------------------------------------------- +// Default export — Task static API surface +// --------------------------------------------------------------------------- + +const Task = {all, allSettled, fromCallback, fromPromise}; +export {Task}; +export default Task; diff --git a/test/browser/components/kepler-gl-test.js b/test/browser/components/kepler-gl-test.js index 26a6489ade..7dec91ca48 100644 --- a/test/browser/components/kepler-gl-test.js +++ b/test/browser/components/kepler-gl-test.js @@ -5,7 +5,7 @@ import React from 'react'; import test from 'tape'; import {mount} from 'enzyme'; import sinon from 'sinon'; -import {drainTasksForTesting, succeedTaskWithValues} from 'react-palm/tasks'; +import {drainTasksForTesting, succeedTaskWithValues} from '@kepler.gl/tasks'; import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; diff --git a/test/helpers/mock-state-utils.js b/test/helpers/mock-state-utils.js index 1382b1e0d1..6680510569 100644 --- a/test/helpers/mock-state-utils.js +++ b/test/helpers/mock-state-utils.js @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import {drainTasksForTesting, succeedTaskWithValues} from 'react-palm/tasks'; +import {drainTasksForTesting, succeedTaskWithValues} from '@kepler.gl/tasks'; import {KeplerTable} from '@kepler.gl/table'; diff --git a/test/node/reducers/composer-state-test.js b/test/node/reducers/composer-state-test.js index ece4c36f85..3c891bc8b6 100644 --- a/test/node/reducers/composer-state-test.js +++ b/test/node/reducers/composer-state-test.js @@ -3,7 +3,7 @@ /* eslint-disable max-statements */ -import {drainTasksForTesting, succeedTaskWithValues} from 'react-palm/tasks'; +import {drainTasksForTesting, succeedTaskWithValues} from '@kepler.gl/tasks'; import test from 'tape'; import {registerEntry} from '@kepler.gl/actions'; diff --git a/test/node/reducers/map-style-test.js b/test/node/reducers/map-style-test.js index 2703db576c..d8b3957f79 100644 --- a/test/node/reducers/map-style-test.js +++ b/test/node/reducers/map-style-test.js @@ -2,7 +2,7 @@ // Copyright contributors to the kepler.gl project import test from 'tape'; -import {drainTasksForTesting, succeedTaskWithValues, errorTaskInTest} from 'react-palm/tasks'; +import {drainTasksForTesting, succeedTaskWithValues, errorTaskInTest} from '@kepler.gl/tasks'; import { mapStyleReducer as reducer, diff --git a/test/node/reducers/provider-state-test.js b/test/node/reducers/provider-state-test.js index 97f0670484..2ff242401d 100644 --- a/test/node/reducers/provider-state-test.js +++ b/test/node/reducers/provider-state-test.js @@ -2,7 +2,7 @@ // Copyright contributors to the kepler.gl project import test from 'tape'; -import {drainTasksForTesting, succeedTaskInTest, errorTaskInTest} from 'react-palm/tasks'; +import {drainTasksForTesting, succeedTaskInTest, errorTaskInTest} from '@kepler.gl/tasks'; import sinon from 'sinon'; import {default as Console} from 'global/console'; diff --git a/test/node/reducers/vis-state-merger-test.js b/test/node/reducers/vis-state-merger-test.js index 8a0f0b1da5..00833ced2b 100644 --- a/test/node/reducers/vis-state-merger-test.js +++ b/test/node/reducers/vis-state-merger-test.js @@ -3,7 +3,7 @@ import test from 'tape'; import cloneDeep from 'lodash/cloneDeep'; -import Task, {withTask, drainTasksForTesting, succeedTaskInTest} from 'react-palm/tasks'; +import Task, {withTask, drainTasksForTesting, succeedTaskInTest} from '@kepler.gl/tasks'; import CloneDeep from 'lodash/cloneDeep'; import keplerGlReducer, { diff --git a/test/node/reducers/vis-state-test.js b/test/node/reducers/vis-state-test.js index 994659e9a2..8d611e35de 100644 --- a/test/node/reducers/vis-state-test.js +++ b/test/node/reducers/vis-state-test.js @@ -6,7 +6,7 @@ import test from 'tape-catch'; import sinon from 'sinon'; import {console as Console} from 'global/window'; -import {drainTasksForTesting, succeedTaskInTest, errorTaskInTest} from 'react-palm/tasks'; +import {drainTasksForTesting, succeedTaskInTest, errorTaskInTest} from '@kepler.gl/tasks'; import CloneDeep from 'lodash/cloneDeep'; import SchemaManager from '@kepler.gl/schemas'; diff --git a/website/package.json b/website/package.json index d59dc0fc28..08f8235c98 100644 --- a/website/package.json +++ b/website/package.json @@ -23,7 +23,6 @@ "prop-types": "^15.6.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-palm": "^3.1.2", "react-redux": "^9.1.0", "react-router-dom": "^6.30.0", "react-waypoint": "^9.0.1", @@ -79,4 +78,4 @@ "yarn": "4.4.0" }, "packageManager": "yarn@4.4.0" -} +} \ No newline at end of file diff --git a/website/src/reducers/index.js b/website/src/reducers/index.js index 657bb76347..67fa225a26 100644 --- a/website/src/reducers/index.js +++ b/website/src/reducers/index.js @@ -2,7 +2,7 @@ // Copyright contributors to the kepler.gl project import {combineReducers, createStore, applyMiddleware, compose} from 'redux'; -import {taskMiddleware} from 'react-palm/tasks'; +import {taskMiddleware} from '@kepler.gl/reducers'; import thunk from 'redux-thunk'; import appReducer from './app'; From 713aed072a9f56b6b1142f4c354da34bbde7339f Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 21:05:19 +0300 Subject: [PATCH 2/9] move to tasks-core Signed-off-by: Ihor Dykhta --- package.json | 1 + src/table/package.json | 2 +- src/table/src/dataset-utils.ts | 3 +- src/tasks-core/babel.config.js | 51 ++++++ src/tasks-core/package.json | 48 ++++++ .../src/index.ts} | 0 src/tasks-core/tsconfig.production.json | 28 ++++ src/tasks/package.json | 3 +- src/tasks/src/index.ts | 6 +- yarn.lock | 149 ++---------------- 10 files changed, 147 insertions(+), 144 deletions(-) create mode 100644 src/tasks-core/babel.config.js create mode 100644 src/tasks-core/package.json rename src/{tasks/src/task-runtime.ts => tasks-core/src/index.ts} (100%) create mode 100644 src/tasks-core/tsconfig.production.json diff --git a/package.json b/package.json index f4d92ae144..1145a96d65 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "./src/schemas", "./src/cloud-providers", "./src/processors", + "./src/tasks-core", "./src/tasks", "./src/actions", "./src/effects", diff --git a/src/table/package.json b/src/table/package.json index 8ee8907598..769276d780 100644 --- a/src/table/package.json +++ b/src/table/package.json @@ -30,7 +30,7 @@ "dependencies": { "@kepler.gl/common-utils": "3.3.0-alpha.4", "@kepler.gl/constants": "3.3.0-alpha.4", - "@kepler.gl/tasks": "3.3.0-alpha.4", + "@kepler.gl/tasks-core": "3.3.0-alpha.4", "@kepler.gl/types": "3.3.0-alpha.4", "@kepler.gl/utils": "3.3.0-alpha.4", "@loaders.gl/mvt": "^4.4.1", diff --git a/src/table/src/dataset-utils.ts b/src/table/src/dataset-utils.ts index f5e7c20e12..0c43edf4f0 100644 --- a/src/table/src/dataset-utils.ts +++ b/src/table/src/dataset-utils.ts @@ -4,7 +4,8 @@ import uniq from 'lodash/uniq'; import KeplerTable, {Datasets} from './kepler-table'; import {ProtoDataset, RGBColor} from '@kepler.gl/types'; -import Task from '@kepler.gl/tasks'; +import Task from '@kepler.gl/tasks-core'; + import { DatasetType, diff --git a/src/tasks-core/babel.config.js b/src/tasks-core/babel.config.js new file mode 100644 index 0000000000..eb16706b69 --- /dev/null +++ b/src/tasks-core/babel.config.js @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +const KeplerPackage = require('./package'); + +const PRESETS = ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']; +const PLUGINS = [ + ['@babel/plugin-transform-typescript', {isTSX: true, allowDeclareFields: true}], + '@babel/plugin-transform-modules-commonjs', + '@babel/plugin-transform-class-properties', + '@babel/plugin-transform-optional-chaining', + '@babel/plugin-transform-logical-assignment-operators', + '@babel/plugin-transform-nullish-coalescing-operator', + '@babel/plugin-transform-export-namespace-from', + [ + '@babel/transform-runtime', + { + regenerator: true + } + ], + [ + 'search-and-replace', + { + rules: [ + { + search: '__PACKAGE_VERSION__', + replace: KeplerPackage.version + } + ] + } + ] +]; +const ENV = { + test: { + plugins: ['istanbul'] + }, + debug: { + sourceMaps: 'inline', + retainLines: true + } +}; + +module.exports = function babel(api) { + api.cache(true); + + return { + presets: PRESETS, + plugins: PLUGINS, + env: ENV + }; +}; diff --git a/src/tasks-core/package.json b/src/tasks-core/package.json new file mode 100644 index 0000000000..7959628292 --- /dev/null +++ b/src/tasks-core/package.json @@ -0,0 +1,48 @@ +{ + "name": "@kepler.gl/tasks-core", + "author": "Shan He ", + "version": "3.3.0-alpha.4", + "description": "Lightweight built-in task runtime for kepler.gl async side effects", + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "keywords": [ + "babel", + "es6", + "react", + "webgl", + "visualization", + "deck.gl" + ], + "repository": { + "type": "git", + "url": "https://github.com/keplergl/kepler.gl.git" + }, + "scripts": { + "build": "rm -fr dist && babel src --out-dir dist --source-maps inline --extensions '.ts,.tsx,.js,.jsx' --ignore '**/*.d.ts'", + "build:types": "tsc --project ./tsconfig.production.json", + "prepublishOnly": "babel-node ../../scripts/license-header/bin --license ../../FILE-HEADER && yarn build && yarn build:types", + "stab": "mkdir -p dist && touch dist/index.js" + }, + "files": [ + "dist" + ], + "nyc": { + "sourceMap": false, + "instrument": false + }, + "maintainers": [ + "Shan He ", + "Giuseppe Macri ", + "Igor Dykhta ", + "Xun Li " + ], + "engines": { + "node": ">=20" + }, + "volta": { + "node": "20.19.3", + "yarn": "4.4.0" + }, + "packageManager": "yarn@4.4.0" +} diff --git a/src/tasks/src/task-runtime.ts b/src/tasks-core/src/index.ts similarity index 100% rename from src/tasks/src/task-runtime.ts rename to src/tasks-core/src/index.ts diff --git a/src/tasks-core/tsconfig.production.json b/src/tasks-core/tsconfig.production.json new file mode 100644 index 0000000000..cc2c36c7df --- /dev/null +++ b/src/tasks-core/tsconfig.production.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2020", + "allowJs": false, + "checkJs": false, + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "declaration": true, + "emitDeclarationOnly": true, + "noImplicitAny": false, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, //TODO change once all dependencies are isolated + "outDir": "dist", + "sourceMap": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": "./src" + }, + "include": ["src"] +} diff --git a/src/tasks/package.json b/src/tasks/package.json index 8ad0721df8..a6e7f0a1cc 100644 --- a/src/tasks/package.json +++ b/src/tasks/package.json @@ -28,7 +28,8 @@ "dist" ], "dependencies": { - "@kepler.gl/processors": "3.3.0-alpha.4" + "@kepler.gl/processors": "3.3.0-alpha.4", + "@kepler.gl/tasks-core": "3.3.0-alpha.4" }, "nyc": { "sourceMap": false, diff --git a/src/tasks/src/index.ts b/src/tasks/src/index.ts index 8c77f03650..4ec2486689 100644 --- a/src/tasks/src/index.ts +++ b/src/tasks/src/index.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import Task, {taskCreator} from './task-runtime'; +import Task, {taskCreator} from '@kepler.gl/tasks-core'; import {readFileInBatches, processFileData} from '@kepler.gl/processors'; export { @@ -20,8 +20,8 @@ export { reportTasksForTesting, all, allSettled -} from './task-runtime'; -export type {TaskDescriptor} from './task-runtime'; +} from '@kepler.gl/tasks-core'; +export type {TaskDescriptor} from '@kepler.gl/tasks-core'; export const LOAD_FILE_TASK = Task.fromPromise( ({file, fileCache, loaders, loadOptions}) => diff --git a/yarn.lock b/yarn.lock index e6ed94d930..4b61496e3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5665,7 +5665,6 @@ __metadata: "@types/react-redux": "npm:^7.1.23" "@types/redux-actions": "npm:^2.6.2" lodash: "npm:4.17.23" - react-palm: "npm:^3.3.8" react-redux: "npm:^8.0.5" redux: "npm:^5.0.0" redux-actions: "npm:^2.2.1" @@ -6099,7 +6098,6 @@ __metadata: deepmerge: "npm:^4.2.2" global: "npm:^4.3.0" lodash: "npm:4.17.23" - react-palm: "npm:^3.3.8" redux: "npm:^5.0.0" redux-actions: "npm:^2.2.1" reselect: "npm:^5.0.0" @@ -6142,6 +6140,7 @@ __metadata: dependencies: "@kepler.gl/common-utils": "npm:3.3.0-alpha.4" "@kepler.gl/constants": "npm:3.3.0-alpha.4" + "@kepler.gl/tasks-core": "npm:3.3.0-alpha.4" "@kepler.gl/types": "npm:3.3.0-alpha.4" "@kepler.gl/utils": "npm:3.3.0-alpha.4" "@loaders.gl/mvt": "npm:^4.4.1" @@ -6152,17 +6151,22 @@ __metadata: global: "npm:^4.3.0" lodash: "npm:4.17.23" moment: "npm:^2.10.6" - react-palm: "npm:^3.3.8" type-analyzer: "npm:0.4.0" languageName: unknown linkType: soft +"@kepler.gl/tasks-core@npm:3.3.0-alpha.4, @kepler.gl/tasks-core@workspace:src/tasks-core": + version: 0.0.0-use.local + resolution: "@kepler.gl/tasks-core@workspace:src/tasks-core" + languageName: unknown + linkType: soft + "@kepler.gl/tasks@npm:3.3.0-alpha.4, @kepler.gl/tasks@workspace:src/tasks": version: 0.0.0-use.local resolution: "@kepler.gl/tasks@workspace:src/tasks" dependencies: "@kepler.gl/processors": "npm:3.3.0-alpha.4" - react-palm: "npm:^3.3.8" + "@kepler.gl/tasks-core": "npm:3.3.0-alpha.4" languageName: unknown linkType: soft @@ -13801,13 +13805,6 @@ __metadata: languageName: node linkType: hard -"asap@npm:~2.0.3": - version: 2.0.6 - resolution: "asap@npm:2.0.6" - checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d - languageName: node - linkType: hard - "asn1.js@npm:^4.10.1": version: 4.10.1 resolution: "asn1.js@npm:4.10.1" @@ -16351,13 +16348,6 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^1.0.0": - version: 1.2.7 - resolution: "core-js@npm:1.2.7" - checksum: 10c0/2b2966e40833f522129da4cc979688760654e9b38e32b06517a94c0edf2882d8990c4b1b0087cb2abfbe6219fdb21dc64343fa264a6b6280ec88c3682b8eee66 - languageName: node - linkType: hard - "core-js@npm:^2.4.0, core-js@npm:^2.6.5": version: 2.6.12 resolution: "core-js@npm:2.6.12" @@ -19458,21 +19448,6 @@ __metadata: languageName: node linkType: hard -"fbjs@npm:^0.8.16": - version: 0.8.18 - resolution: "fbjs@npm:0.8.18" - dependencies: - core-js: "npm:^1.0.0" - isomorphic-fetch: "npm:^2.1.1" - loose-envify: "npm:^1.0.0" - object-assign: "npm:^4.1.0" - promise: "npm:^7.1.1" - setimmediate: "npm:^1.0.5" - ua-parser-js: "npm:^0.7.30" - checksum: 10c0/a7e1c64c349cde000e5d94ce0289c59b725a95fbdacc22529155c4dacea1dde37a4dae7e16f0f6602dca566f15978b42acd7ee973b620eaac612b5228687ffe0 - languageName: node - linkType: hard - "fd-slicer@npm:~1.1.0": version: 1.1.0 resolution: "fd-slicer@npm:1.1.0" @@ -20083,7 +20058,7 @@ __metadata: languageName: node linkType: hard -"function.prototype.name@npm:^1.1.0, function.prototype.name@npm:^1.1.5, function.prototype.name@npm:^1.1.6": +"function.prototype.name@npm:^1.1.5, function.prototype.name@npm:^1.1.6": version: 1.1.6 resolution: "function.prototype.name@npm:1.1.6" dependencies: @@ -22770,16 +22745,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-fetch@npm:^2.1.1": - version: 2.2.1 - resolution: "isomorphic-fetch@npm:2.2.1" - dependencies: - node-fetch: "npm:^1.0.1" - whatwg-fetch: "npm:>=0.10.0" - checksum: 10c0/ea9fd37d31ec7b35b82180e1946d4a2f512506d0559fa567ec6ee6701ff1c6d924be90e75499c50982274b707e03ecd9eaa21d618872dd0deff530e4c3bdb074 - languageName: node - linkType: hard - "isomorphic-ws@npm:^4.0.1": version: 4.0.1 resolution: "isomorphic-ws@npm:4.0.1" @@ -24549,7 +24514,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -28748,15 +28713,6 @@ __metadata: languageName: node linkType: hard -"promise@npm:^7.1.1": - version: 7.3.1 - resolution: "promise@npm:7.3.1" - dependencies: - asap: "npm:~2.0.3" - checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f - languageName: node - linkType: hard - "prompts@npm:^2.0.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -29279,20 +29235,6 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^16.4.2": - version: 16.14.0 - resolution: "react-dom@npm:16.14.0" - dependencies: - loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - prop-types: "npm:^15.6.2" - scheduler: "npm:^0.19.1" - peerDependencies: - react: ^16.14.0 - checksum: 10c0/ca146e780631672a2d57c8d77775d38f394a6cd67db30c6af7964d0b3574ef7edccb1de8d592e990b98f4f5f8d1c8460b0691f04e7a45799962a51dcbaaa1371 - languageName: node - linkType: hard - "react-dom@npm:^19.0.0": version: 19.2.7 resolution: "react-dom@npm:19.2.7" @@ -29378,7 +29320,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.6": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 @@ -29505,37 +29447,6 @@ __metadata: languageName: node linkType: hard -"react-palm@npm:^3.3.8": - version: 3.3.8 - resolution: "react-palm@npm:3.3.8" - dependencies: - function.prototype.name: "npm:^1.1.0" - react-dom: "npm:^16.4.2" - react-reconciler: "npm:^0.12.0" - react-test-renderer: "npm:^16.4.2" - peerDependencies: - enzyme: ^3.6.0 - enzyme-adapter-utils: ^1.13.0 - react: ^16.4.1 - react-test-renderer: ^16.4.1 - checksum: 10c0/dd052acbeff68e55f184f2c181d5ceb9aa129598ff3116fa9e5921b4d720957e0314efe7346620e6f1971cbfc574f39fd3d79b1659b8bf560feb5f54fe03a7c5 - languageName: node - linkType: hard - -"react-reconciler@npm:^0.12.0": - version: 0.12.0 - resolution: "react-reconciler@npm:0.12.0" - dependencies: - fbjs: "npm:^0.8.16" - loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - prop-types: "npm:^15.6.0" - peerDependencies: - react: ^16.0.0 - checksum: 10c0/2e3262b276751465fb833ef6c989cee0716742c2d90a1bafe574f19d8cacefdc3b94dda0f25c3e664711ca8b25d35a033a3bafa21d733d7334c2611d81a54694 - languageName: node - linkType: hard - "react-redux@npm:^8.0.5": version: 8.1.3 resolution: "react-redux@npm:8.1.3" @@ -29597,20 +29508,6 @@ __metadata: languageName: node linkType: hard -"react-test-renderer@npm:^16.4.2": - version: 16.14.0 - resolution: "react-test-renderer@npm:16.14.0" - dependencies: - object-assign: "npm:^4.1.1" - prop-types: "npm:^15.6.2" - react-is: "npm:^16.8.6" - scheduler: "npm:^0.19.1" - peerDependencies: - react: ^16.14.0 - checksum: 10c0/119e3ce5509c3443393ca750e39dd4ac9ee9ddfaafca58c9067b477447edc2badb75660b9fea7e9ddef012e37bbba427681cf6f8d3fde61b8054655a133bfbf5 - languageName: node - linkType: hard - "react-textarea-autosize@npm:^8.5.3": version: 8.5.5 resolution: "react-textarea-autosize@npm:8.5.5" @@ -30864,16 +30761,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.19.1": - version: 0.19.1 - resolution: "scheduler@npm:0.19.1" - dependencies: - loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - checksum: 10c0/9658932a73148a93d791c064b331d9690ddfecc4de25bcd6c9b89f5f1166e3d23d9c31c1595d66565e5ffbb34d47035cb14841aba6444bc266bfcd215cefe9c0 - languageName: node - linkType: hard - "scheduler@npm:^0.27.0": version: 0.27.0 resolution: "scheduler@npm:0.27.0" @@ -33766,13 +33653,6 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.30": - version: 0.7.38 - resolution: "ua-parser-js@npm:0.7.38" - checksum: 10c0/da963eae1618f0c60d0812851a4d478fb8bb127ee6e5c566b8dac27eeb25757d818d9ade2c312d73018f2bb3c3e629d26c066fcda3cb9d55a31289c9566198df - languageName: node - linkType: hard - "uglify-js@npm:^3.1.4": version: 3.19.0 resolution: "uglify-js@npm:3.19.0" @@ -35019,13 +34899,6 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:>=0.10.0": - version: 3.6.20 - resolution: "whatwg-fetch@npm:3.6.20" - checksum: 10c0/fa972dd14091321d38f36a4d062298df58c2248393ef9e8b154493c347c62e2756e25be29c16277396046d6eaa4b11bd174f34e6403fff6aaca9fb30fa1ff46d - languageName: node - linkType: hard - "whatwg-mimetype@npm:^3.0.0": version: 3.0.0 resolution: "whatwg-mimetype@npm:3.0.0" From 28119a9c10fea29e4057ae4aef7d1d6caa2ea346 Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 21:08:25 +0300 Subject: [PATCH 3/9] fix: correct taskMiddleware import paths in docs and website taskMiddleware is exported from @kepler.gl/tasks, not @kepler.gl/reducers. @kepler.gl/reducers only exports enhanceReduxMiddleware. Update JSDoc examples in root.ts and action-types.ts, website store setup, and UPGRADE-GUIDE.md highlight bullet. Co-authored-by: Cursor --- UPGRADE-GUIDE.md | 2 +- src/actions/src/action-types.ts | 2 +- src/reducers/src/root.ts | 2 +- website/src/reducers/index.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md index f9c10f5c5f..46d48f8c40 100644 --- a/UPGRADE-GUIDE.md +++ b/UPGRADE-GUIDE.md @@ -27,7 +27,7 @@ See the full upgrade guide: **[Upgrade Guide — kepler.gl 3.3](docs/upgrade-gui - **HeatmapLayer** — rewritten from Mapbox GL to deck.gl base - **`layerOrder`** — type changed from flat `string[]` to `(string | LayerOrderGroup)[]` - **`LayerSelectorPanelFactory`** — removed from `@kepler.gl/components` -- **`react-palm` removed** — replace `import {taskMiddleware} from 'react-palm/tasks'` with `import {taskMiddleware} from '@kepler.gl/reducers'` +- **`react-palm` removed** — replace `import {taskMiddleware} from 'react-palm/tasks'` with `import {taskMiddleware} from '@kepler.gl/tasks'` ## Upgrade from v2.4 to v3.0 diff --git a/src/actions/src/action-types.ts b/src/actions/src/action-types.ts index ea72543881..b68c0341f8 100644 --- a/src/actions/src/action-types.ts +++ b/src/actions/src/action-types.ts @@ -13,7 +13,7 @@ export const ACTION_PREFIX = '@@kepler.gl/'; * // store.js * import {handleActions} from 'redux-actions'; * import {createStore, combineReducers, applyMiddleware} from 'redux'; - * import {taskMiddleware} from '@kepler.gl/reducers'; + * import {taskMiddleware} from '@kepler.gl/tasks'; * * import keplerGlReducer from '@kepler.gl/reducers'; * import {ActionTypes} from '@kepler.gl/actions'; diff --git a/src/reducers/src/root.ts b/src/reducers/src/root.ts index 7237ec3d17..baea63c09c 100644 --- a/src/reducers/src/root.ts +++ b/src/reducers/src/root.ts @@ -231,7 +231,7 @@ function decorate(target, savedInitialState = {}) { * @example * import keplerGlReducer from '@kepler.gl/reducers'; * import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; - * import {taskMiddleware} from '@kepler.gl/reducers'; + * import {taskMiddleware} from '@kepler.gl/tasks'; * * const initialState = {}; * const reducers = combineReducers({ diff --git a/website/src/reducers/index.js b/website/src/reducers/index.js index 67fa225a26..2fdd2e1241 100644 --- a/website/src/reducers/index.js +++ b/website/src/reducers/index.js @@ -2,7 +2,7 @@ // Copyright contributors to the kepler.gl project import {combineReducers, createStore, applyMiddleware, compose} from 'redux'; -import {taskMiddleware} from '@kepler.gl/reducers'; +import {taskMiddleware} from '@kepler.gl/tasks'; import thunk from 'redux-thunk'; import appReducer from './app'; From 9071f3d34a977a4320ae0bee9c798b2b66446d91 Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 21:33:08 +0300 Subject: [PATCH 4/9] lint Signed-off-by: Ihor Dykhta --- src/reducers/src/provider-state-updaters.ts | 5 +++-- src/reducers/src/vis-state-updaters.ts | 7 ++++--- src/table/src/dataset-utils.ts | 3 ++- src/tasks-core/src/index.ts | 14 +++++++------- src/tasks/src/index.ts | 2 ++ 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/reducers/src/provider-state-updaters.ts b/src/reducers/src/provider-state-updaters.ts index 36c2f2eb71..486be95a5e 100644 --- a/src/reducers/src/provider-state-updaters.ts +++ b/src/reducers/src/provider-state-updaters.ts @@ -2,6 +2,7 @@ // Copyright contributors to the kepler.gl project import Task, {withTask} from '@kepler.gl/tasks'; +import type {TaskDescriptor} from '@kepler.gl/tasks'; import Console from 'global/console'; import {getApplicationConfig, getError, isPlainObject} from '@kepler.gl/utils'; import {generateHashId, toArray} from '@kepler.gl/common-utils'; @@ -166,7 +167,7 @@ export const exportFileSuccessUpdater = ( createActionTask(onSuccess, {response, provider, options}), closeModal && ACTION_TASK().map(() => postSaveLoadSuccess(`Map saved to ${state.currentProvider}!`)) - ].filter(d => d); + ].filter((d): d is TaskDescriptor => Boolean(d)); return tasks.length ? withTask(newState, tasks) : newState; }; @@ -407,7 +408,7 @@ export const loadCloudMapSuccess2Updater = ( ACTION_TASK().map(() => addDataToMap(datasetsPayload)), createActionTask(onSuccess, {response, loadParams, provider}), ACTION_TASK().map(() => postSaveLoadSuccess(`Map from ${provider.name} loaded`)) - ].filter(d => d); + ].filter((d): d is TaskDescriptor => Boolean(d)); return tasks.length ? withTask(newState, tasks) : newState; }; diff --git a/src/reducers/src/vis-state-updaters.ts b/src/reducers/src/vis-state-updaters.ts index 3b606965c2..4850ca8453 100644 --- a/src/reducers/src/vis-state-updaters.ts +++ b/src/reducers/src/vis-state-updaters.ts @@ -13,6 +13,7 @@ import pick from 'lodash/pick'; import uniq from 'lodash/uniq'; import xor from 'lodash/xor'; import Task, {disableStackCapturing, withTask} from '@kepler.gl/tasks'; +import type {TaskDescriptor} from '@kepler.gl/tasks'; // Tasks import { DELAY_TASK, @@ -2901,8 +2902,8 @@ export const updateVisDataUpdater = ( return updatedState; } - const createDatasetTasks: Task[] = []; - const notificationTasks: Task[] = []; + const createDatasetTasks: TaskDescriptor[] = []; + const notificationTasks: TaskDescriptor[] = []; datasets.forEach(({info = {}, ...rest}, datasetIndex) => { const task = createNewDataEntry({info, ...rest}, state.datasets); @@ -2943,7 +2944,7 @@ export const createNewDatasetSuccessUpdater = ( action: PayloadAction ): VisState => { const {results, addToMapOptions} = action.payload; - const notificationTasks: Task[] = []; + const notificationTasks: TaskDescriptor[] = []; const newDataEntries = results.reduce((accu, result, idx) => { if (result.status === 'fulfilled') { diff --git a/src/table/src/dataset-utils.ts b/src/table/src/dataset-utils.ts index 0c43edf4f0..affbec20af 100644 --- a/src/table/src/dataset-utils.ts +++ b/src/table/src/dataset-utils.ts @@ -5,6 +5,7 @@ import uniq from 'lodash/uniq'; import KeplerTable, {Datasets} from './kepler-table'; import {ProtoDataset, RGBColor} from '@kepler.gl/types'; import Task from '@kepler.gl/tasks-core'; +import type {TaskDescriptor} from '@kepler.gl/tasks-core'; import { @@ -72,7 +73,7 @@ export function getNewDatasetColor(datasets: Datasets): RGBColor { export function createNewDataEntry( {info, data, ...opts}: ProtoDataset, datasets: Datasets = {} -): Datasets | null { +): TaskDescriptor | null { const TableClass = getApplicationConfig().table ?? KeplerTable; let dataValidator = validateInputData; if (typeof TableClass.getInputDataValidator === 'function') { diff --git a/src/tasks-core/src/index.ts b/src/tasks-core/src/index.ts index 01182008f3..d0353de185 100644 --- a/src/tasks-core/src/index.ts +++ b/src/tasks-core/src/index.ts @@ -49,8 +49,8 @@ export type TaskDescriptor = { chain: (fn: (value: any) => TaskDescriptor) => TaskDescriptor; }; -/** A factory function that creates a TaskDescriptor from an argument */ -export type TaskFactory = ((arg: any) => TaskDescriptor) & {label?: string; type?: string}; +/** A factory function that creates a TaskDescriptor from an optional argument */ +export type TaskFactory = ((arg?: any) => TaskDescriptor) & {label?: string; type?: string}; type InstrumentHook = ( event: 'start' | 'success' | 'error', @@ -166,9 +166,9 @@ function wrapWithInstrumentation( * // in a reducer: * return withTask(newState, FETCH_DATA(url).bimap(onSuccess, onError)); */ -export function fromPromise(fn: (arg: any) => Promise, label: string): TaskFactory { +export function fromPromise(fn: (arg?: any) => Promise, label: string): TaskFactory { return Object.assign( - (arg: any) => + (arg?: any) => wrapWithInstrumentation( (resolve, reject) => fn(arg).then(resolve, reject), arg, @@ -183,11 +183,11 @@ export function fromPromise(fn: (arg: any) => Promise, label: string): Task * The callback receives `(error, result)`. */ export function fromCallback( - fn: (arg: any, done: (err: any, result?: any) => void) => any, + fn: (arg: any, done: (err?: any, result?: any) => void) => any, label: string ): TaskFactory { return Object.assign( - (arg: any) => + (arg?: any) => wrapWithInstrumentation( (resolve, reject) => fn(arg, (err, result) => (err ? reject(err) : resolve(result))), arg, @@ -203,7 +203,7 @@ export function fromCallback( */ export function taskCreator(fn: Function, label: string): TaskFactory { return Object.assign( - (arg: any) => + (arg?: any) => wrapWithInstrumentation( (resolve, reject) => fn(arg, resolve, reject), arg, diff --git a/src/tasks/src/index.ts b/src/tasks/src/index.ts index 4ec2486689..bfd7d5f136 100644 --- a/src/tasks/src/index.ts +++ b/src/tasks/src/index.ts @@ -87,3 +87,5 @@ export const UNWRAP_TASK = Task.fromPromise( 'UNWRAP' ); + +export default Task; From eb0d6ba52a34901f4c1adb256d3d423f66e85e40 Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 21:46:30 +0300 Subject: [PATCH 5/9] lint Signed-off-by: Ihor Dykhta --- src/tasks-core/src/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tasks-core/src/index.ts b/src/tasks-core/src/index.ts index d0353de185..c8a449d998 100644 --- a/src/tasks-core/src/index.ts +++ b/src/tasks-core/src/index.ts @@ -62,7 +62,7 @@ type InstrumentHook = ( // Instrumentation (test hook) // --------------------------------------------------------------------------- -let instrumentHook: InstrumentHook = () => {}; +let instrumentHook: InstrumentHook = (_event, _task, _value) => undefined; /** * Override the instrumentation hook — used in tests to observe task @@ -128,9 +128,11 @@ function wrapWithInstrumentation( payload: any, label: string ): TaskDescriptor { - let self: TaskDescriptor; - const traced = Object.assign( - (resolve: ResolveFn, reject: RejectFn, ctx?: ProgressCtx) => { + // `self` is const; the closures below only run after buildTask() returns, + // so the binding is fully initialised before any closure body executes. + const self: TaskDescriptor = buildTask( + payload, + (_runner, resolve, reject, ctx) => { instrumentHook('start', self, payload); return effect( value => { @@ -144,11 +146,6 @@ function wrapWithInstrumentation( ctx ); }, - {payload, label} - ); - self = buildTask( - payload, - (runner, resolve, reject, ctx) => runner(traced, resolve, reject, ctx), label ); return self; @@ -201,7 +198,10 @@ export function fromCallback( * Create a task factory from a function that accepts explicit * `(arg, resolve, reject)` parameters. */ -export function taskCreator(fn: Function, label: string): TaskFactory { +export function taskCreator( + fn: (arg?: any, resolve?: ResolveFn, reject?: RejectFn) => any, + label: string +): TaskFactory { return Object.assign( (arg?: any) => wrapWithInstrumentation( From 0224976722fea9493956b40c6766ff8902aa480b Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 22:02:29 +0300 Subject: [PATCH 6/9] lint Signed-off-by: Ihor Dykhta --- src/tasks-core/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks-core/src/index.ts b/src/tasks-core/src/index.ts index c8a449d998..ddd4cf4ec6 100644 --- a/src/tasks-core/src/index.ts +++ b/src/tasks-core/src/index.ts @@ -199,7 +199,7 @@ export function fromCallback( * `(arg, resolve, reject)` parameters. */ export function taskCreator( - fn: (arg?: any, resolve?: ResolveFn, reject?: RejectFn) => any, + fn: (arg: any, resolve: ResolveFn, reject: RejectFn) => any, label: string ): TaskFactory { return Object.assign( From 2235850200f77f3148c9ce5eec24266331cdfc0f Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 22:20:31 +0300 Subject: [PATCH 7/9] fix yarn cover Signed-off-by: Ihor Dykhta --- src/tasks-core/src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/tasks-core/src/index.ts b/src/tasks-core/src/index.ts index ddd4cf4ec6..8eb4e20916 100644 --- a/src/tasks-core/src/index.ts +++ b/src/tasks-core/src/index.ts @@ -247,11 +247,7 @@ function combineParallel(tasks: TaskDescriptor[], settled: boolean): TaskDescrip if (remaining === 0) resolve(results); }; - return Promise.allSettled( - tasks.map((task, i) => - task.run(runner, onResolved(i), onRejected(i), ctx) - ) - ); + tasks.forEach((task, i) => task.run(runner, onResolved(i), onRejected(i), ctx)); }, label ); From 88ab494c8bdc040ce913910b38fdc5a11f40f86b Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 23:26:46 +0300 Subject: [PATCH 8/9] try to fix tests Signed-off-by: Ihor Dykhta --- src/tasks-core/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tasks-core/src/index.ts b/src/tasks-core/src/index.ts index 8eb4e20916..d38161f620 100644 --- a/src/tasks-core/src/index.ts +++ b/src/tasks-core/src/index.ts @@ -132,9 +132,10 @@ function wrapWithInstrumentation( // so the binding is fully initialised before any closure body executes. const self: TaskDescriptor = buildTask( payload, - (_runner, resolve, reject, ctx) => { + (runner, resolve, reject, ctx) => { instrumentHook('start', self, payload); - return effect( + return runner( + effect, value => { instrumentHook('success', self, value); return resolve(value); From 69d6ff11c6af68b16d4e033c9196020b62cc2bc7 Mon Sep 17 00:00:00 2001 From: Ihor Dykhta Date: Wed, 29 Jul 2026 23:43:47 +0300 Subject: [PATCH 9/9] stale website yarn lock Signed-off-by: Ihor Dykhta --- website/package.json | 2 +- website/yarn.lock | 170 ++----------------------------------------- 2 files changed, 7 insertions(+), 165 deletions(-) diff --git a/website/package.json b/website/package.json index 08f8235c98..b099138645 100644 --- a/website/package.json +++ b/website/package.json @@ -78,4 +78,4 @@ "yarn": "4.4.0" }, "packageManager": "yarn@4.4.0" -} \ No newline at end of file +} diff --git a/website/yarn.lock b/website/yarn.lock index 0c578ad6bd..16fc17f4e3 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2994,13 +2994,6 @@ __metadata: languageName: node linkType: hard -"asap@npm:~2.0.3": - version: 2.0.6 - resolution: "asap@npm:2.0.6" - checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d - languageName: node - linkType: hard - "asn1js@npm:^3.0.6": version: 3.0.10 resolution: "asn1js@npm:3.0.10" @@ -3585,13 +3578,6 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^1.0.0": - version: 1.2.7 - resolution: "core-js@npm:1.2.7" - checksum: 10c0/2b2966e40833f522129da4cc979688760654e9b38e32b06517a94c0edf2882d8990c4b1b0087cb2abfbe6219fdb21dc64343fa264a6b6280ec88c3682b8eee66 - languageName: node - linkType: hard - "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -3934,7 +3920,7 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.11, encoding@npm:^0.1.13": +"encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -4642,21 +4628,6 @@ __metadata: languageName: node linkType: hard -"fbjs@npm:^0.8.16": - version: 0.8.18 - resolution: "fbjs@npm:0.8.18" - dependencies: - core-js: "npm:^1.0.0" - isomorphic-fetch: "npm:^2.1.1" - loose-envify: "npm:^1.0.0" - object-assign: "npm:^4.1.0" - promise: "npm:^7.1.1" - setimmediate: "npm:^1.0.5" - ua-parser-js: "npm:^0.7.30" - checksum: 10c0/a7e1c64c349cde000e5d94ce0289c59b725a95fbdacc22529155c4dacea1dde37a4dae7e16f0f6602dca566f15978b42acd7ee973b620eaac612b5228687ffe0 - languageName: node - linkType: hard - "figures@npm:^2.0.0": version: 2.0.0 resolution: "figures@npm:2.0.0" @@ -4870,7 +4841,7 @@ __metadata: languageName: node linkType: hard -"function.prototype.name@npm:^1.1.0, function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": +"function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": version: 1.1.8 resolution: "function.prototype.name@npm:1.1.8" dependencies: @@ -5639,13 +5610,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^1.0.1": - version: 1.1.0 - resolution: "is-stream@npm:1.1.0" - checksum: 10c0/b8ae7971e78d2e8488d15f804229c6eed7ed36a28f8807a1815938771f4adff0e705218b7dab968270433f67103e4fef98062a0beea55d64835f705ee72c7002 - languageName: node - linkType: hard - "is-string@npm:^1.0.7, is-string@npm:^1.1.1": version: 1.1.1 resolution: "is-string@npm:1.1.1" @@ -5746,16 +5710,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-fetch@npm:^2.1.1": - version: 2.2.1 - resolution: "isomorphic-fetch@npm:2.2.1" - dependencies: - node-fetch: "npm:^1.0.1" - whatwg-fetch: "npm:>=0.10.0" - checksum: 10c0/ea9fd37d31ec7b35b82180e1946d4a2f512506d0559fa567ec6ee6701ff1c6d924be90e75499c50982274b707e03ecd9eaa21d618872dd0deff530e4c3bdb074 - languageName: node - linkType: hard - "iterator.prototype@npm:^1.1.4": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -5933,7 +5887,6 @@ __metadata: prop-types: "npm:^15.6.1" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" - react-palm: "npm:^3.1.2" react-redux: "npm:^9.1.0" react-router-dom: "npm:^6.30.0" react-waypoint: "npm:^9.0.1" @@ -6051,7 +6004,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -6479,16 +6432,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^1.0.1": - version: 1.7.3 - resolution: "node-fetch@npm:1.7.3" - dependencies: - encoding: "npm:^0.1.11" - is-stream: "npm:^1.0.1" - checksum: 10c0/5a6b56b3edf909ccd20414355867d24f15f1885da3b26be90840241c46e63754ebf4697050f897daab676e3952d969611ffe1d4bc4506cf50f70837e20ad5328 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 11.0.0 resolution: "node-gyp@npm:11.0.0" @@ -6541,7 +6484,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 @@ -7041,16 +6984,7 @@ __metadata: languageName: node linkType: hard -"promise@npm:^7.1.1": - version: 7.3.1 - resolution: "promise@npm:7.3.1" - dependencies: - asap: "npm:~2.0.3" - checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f - languageName: node - linkType: hard - -"prop-types@npm:^15.0.0, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.0.0, prop-types@npm:^15.6.1, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -7131,20 +7065,6 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^16.4.2": - version: 16.14.0 - resolution: "react-dom@npm:16.14.0" - dependencies: - loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - prop-types: "npm:^15.6.2" - scheduler: "npm:^0.19.1" - peerDependencies: - react: ^16.14.0 - checksum: 10c0/ca146e780631672a2d57c8d77775d38f394a6cd67db30c6af7964d0b3574ef7edccb1de8d592e990b98f4f5f8d1c8460b0691f04e7a45799962a51dcbaaa1371 - languageName: node - linkType: hard - "react-dom@npm:^19.0.0": version: 19.2.7 resolution: "react-dom@npm:19.2.7" @@ -7156,44 +7076,13 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.6.3, react-is@npm:^16.8.6": +"react-is@npm:^16.13.1, react-is@npm:^16.6.3": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 languageName: node linkType: hard -"react-palm@npm:^3.1.2": - version: 3.3.8 - resolution: "react-palm@npm:3.3.8" - dependencies: - function.prototype.name: "npm:^1.1.0" - react-dom: "npm:^16.4.2" - react-reconciler: "npm:^0.12.0" - react-test-renderer: "npm:^16.4.2" - peerDependencies: - enzyme: ^3.6.0 - enzyme-adapter-utils: ^1.13.0 - react: ^16.4.1 - react-test-renderer: ^16.4.1 - checksum: 10c0/dd052acbeff68e55f184f2c181d5ceb9aa129598ff3116fa9e5921b4d720957e0314efe7346620e6f1971cbfc574f39fd3d79b1659b8bf560feb5f54fe03a7c5 - languageName: node - linkType: hard - -"react-reconciler@npm:^0.12.0": - version: 0.12.0 - resolution: "react-reconciler@npm:0.12.0" - dependencies: - fbjs: "npm:^0.8.16" - loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - prop-types: "npm:^15.6.0" - peerDependencies: - react: ^16.0.0 - checksum: 10c0/2e3262b276751465fb833ef6c989cee0716742c2d90a1bafe574f19d8cacefdc3b94dda0f25c3e664711ca8b25d35a033a3bafa21d733d7334c2611d81a54694 - languageName: node - linkType: hard - "react-redux@npm:^9.1.0": version: 9.3.0 resolution: "react-redux@npm:9.3.0" @@ -7237,20 +7126,6 @@ __metadata: languageName: node linkType: hard -"react-test-renderer@npm:^16.4.2": - version: 16.14.0 - resolution: "react-test-renderer@npm:16.14.0" - dependencies: - object-assign: "npm:^4.1.1" - prop-types: "npm:^15.6.2" - react-is: "npm:^16.8.6" - scheduler: "npm:^0.19.1" - peerDependencies: - react: ^16.14.0 - checksum: 10c0/119e3ce5509c3443393ca750e39dd4ac9ee9ddfaafca58c9067b477447edc2badb75660b9fea7e9ddef012e37bbba427681cf6f8d3fde61b8054655a133bfbf5 - languageName: node - linkType: hard - "react-waypoint@npm:^9.0.1": version: 9.0.3 resolution: "react-waypoint@npm:9.0.3" @@ -7675,16 +7550,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.19.1": - version: 0.19.1 - resolution: "scheduler@npm:0.19.1" - dependencies: - loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - checksum: 10c0/9658932a73148a93d791c064b331d9690ddfecc4de25bcd6c9b89f5f1166e3d23d9c31c1595d66565e5ffbb34d47035cb14841aba6444bc266bfcd215cefe9c0 - languageName: node - linkType: hard - "scheduler@npm:^0.27.0": version: 0.27.0 resolution: "scheduler@npm:0.27.0" @@ -7866,13 +7731,6 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.5": - version: 1.0.5 - resolution: "setimmediate@npm:1.0.5" - checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 - languageName: node - linkType: hard - "setprototypeof@npm:1.1.0": version: 1.1.0 resolution: "setprototypeof@npm:1.1.0" @@ -8616,15 +8474,6 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.30": - version: 0.7.40 - resolution: "ua-parser-js@npm:0.7.40" - bin: - ua-parser-js: script/cli.js - checksum: 10c0/d114f0b71b5b0106dcc0cb7cc26a44690073e886fa1444f8c03131d4f57b3f6645f9fb7b308b0aaaa5a2774461f9e8fe1a2a1c3ff69aa531316fcf14cd44dbe3 - languageName: node - linkType: hard - "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -9005,13 +8854,6 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:>=0.10.0": - version: 3.6.20 - resolution: "whatwg-fetch@npm:3.6.20" - checksum: 10c0/fa972dd14091321d38f36a4d062298df58c2248393ef9e8b154493c347c62e2756e25be29c16277396046d6eaa4b11bd174f34e6403fff6aaca9fb30fa1ff46d - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1"