From d33a3775e92a547945a83233c7b7fd92eb2fbded Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 11:17:06 +0900 Subject: [PATCH 01/16] Revive Halo with scoped task scheduling --- .github/workflows/ci.yml | 41 +- .gitignore | 14 +- README.md | 288 +- bower.json | 37 - package-lock.json | 7584 +++++---------------- package.json | 27 +- packages.dhall | 5 - spago.dhall | 27 - spago.lock | 1940 ++++++ spago.test.dhall | 10 - spago.yaml | 38 + src/React/Halo.purs | 6 +- src/React/Halo/Component.purs | 54 +- src/React/Halo/Hook.purs | 93 +- src/React/Halo/Internal/Control.purs | 209 +- src/React/Halo/Internal/Eval.purs | 140 +- src/React/Halo/Internal/Runtime.purs | 620 ++ src/React/Halo/Internal/State.purs | 46 - src/React/Halo/Internal/Types.purs | 81 +- test/Main.purs | 136 +- test/Test/Halo/DocExamples.purs | 101 + test/Test/Halo/Helpers.purs | 154 + test/Test/Halo/LifecycleSpec.purs | 238 + test/Test/Halo/SchedulerSpec.purs | 145 + test/Test/Halo/SubscriptionErrorSpec.purs | 128 + 25 files changed, 5451 insertions(+), 6711 deletions(-) delete mode 100644 bower.json delete mode 100644 packages.dhall delete mode 100644 spago.dhall create mode 100644 spago.lock delete mode 100644 spago.test.dhall create mode 100644 spago.yaml create mode 100644 src/React/Halo/Internal/Runtime.purs delete mode 100644 src/React/Halo/Internal/State.purs create mode 100644 test/Test/Halo/DocExamples.purs create mode 100644 test/Test/Halo/Helpers.purs create mode 100644 test/Test/Halo/LifecycleSpec.purs create mode 100644 test/Test/Halo/SchedulerSpec.purs create mode 100644 test/Test/Halo/SubscriptionErrorSpec.purs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fffc545..2b1e933 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,26 +1,35 @@ name: CI -on: push +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + contents: read jobs: - build: + build-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-node@v3 + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: thomashoneyman/setup-purescript@main + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - purescript: "0.15.4" - spago: "0.20.9" + node-version: 22 + cache: npm - - name: Cache PureScript dependencies - uses: actions/cache@v2 - with: - key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }} - path: | - .spago - output + - name: Install pinned tools + run: npm ci + + - name: Check formatting + run: npm run format:check + + - name: Build + run: npm run build -- --strict - - run: npm test + - name: Test + run: npm test diff --git a/.gitignore b/.gitignore index 77f2c6a..50268d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,5 @@ -/bower_components/ /node_modules/ -/.pulp-cache/ /output/ -/generated-docs/* -!/generated-docs/md -/generated-docs/md/* -!/generated-docs/md/React.Halo.* -/.psc-package/ -/.psc* -/.purs* -/.psa* -/.spago +/.spago/ +/.spec-results +/generated-docs/ diff --git a/README.md b/README.md index 3bddd76..dbb8047 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,276 @@ # React Halo -Halo is a [Halogen](https://github.com/purescript-halogen/purescript-halogen)-inspired interface for React. +Halo gives PureScript React components one typed action loop for state and asynchronous effects, with component-scoped cancellation and explicit concurrency policies. -It is available as a hook: `useHalo`; for building entire components there is `component`. +Use Halo when a component has event-driven workflows that are awkward to express as independent hooks: rapid searches that must replace stale requests, saves that must not overlap, ordered uploads, or bursts where only the newest pending action matters. For a single request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. -## Documentation +## Install -Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-react-halo). +Halo v4 targets PureScript 0.15.16, Spago 1.0.4, and the Registry package set 80.8.0 used by this repository. -## Using with [Spago](https://github.com/purescript/spago) +```console +spago install react-halo +``` -`$ spago install react-halo` -or -`$ npx spago install react-halo` +Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo does not publish an npm runtime entry point. -## What does Halo provide? +## Quick start: a restartable request -Whether you are using the hook or one of the component helpers, the main feature that Halo provides is the `eval` function. It looks like: +This complete component starts `loadGreeting` when the button is clicked. Clicking again while the request is running cancels the previous Halo task. Even if underlying work cannot be interrupted, the replaced task cannot commit Halo state. ```purescript -Lifecycle props action -> HaloM props state action m Unit +module Example.LoadButton where + +import Prelude + +import Control.Monad.State (modify_) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Effect.Aff (Aff, attempt) +import Effect.Aff.Class (liftAff) +import Effect.Class.Console as Console +import Effect.Exception (message) +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) +import React.Basic.Hooks (Component) +import React.Halo as Halo + +newtype Props = Props { loadGreeting :: Aff String } + +type State = + { loading :: Boolean + , result :: Maybe (Either String String) + } + +data Action = Load + +data Task = GreetingRequest + +derive instance eqTask :: Eq Task +derive instance ordTask :: Ord Task + +loadButton :: Component Props +loadButton = Halo.component "LoadButton" + { initialState: \_ -> { loading: false, result: Nothing } + , schedule: \Load -> Halo.Restartable GreetingRequest + , eval: case _ of + Halo.Action Load -> do + modify_ _ { loading = true, result = Nothing } + Props { loadGreeting } <- Halo.props + outcome <- liftAff $ attempt loadGreeting + modify_ _ + { loading = false + , result = Just $ case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + } + _ -> pure unit + , onError: \_ error -> + Console.error $ "Unexpected Halo failure: " <> message error + , render: \{ state, dispatch, activity } -> + let counts = Halo.activityFor GreetingRequest activity + in R.div_ + [ R.button + { onClick: capture_ (dispatch Load) + , children: + [ R.text if counts.running > 0 then "Restart load" else "Load" ] + } + , R.text $ case state.result of + Nothing -> if state.loading then "Loading…" else "Not loaded" + Just (Left error) -> error + Just (Right greeting) -> greeting + ] + } ``` -where `Lifecycle` is: +The example catches an expected request failure and stores it in domain state. `onError` is for unexpected failures that escape `eval`. -```purescript -data Lifecycle props action - = Initialize -- when the component mounts - | Update props -- when the props change, passing the previous props - | Action action -- when an action is dispatched, passing the action - | Finalize -- when the component unmounts -``` +## Schedule actions by intent + +The `schedule` function assigns each dispatched action a policy. Keys are an application-defined type with an `Ord` instance; actions with the same key coordinate with one another. -The helper `mkEval` exists to make this easier to work with: +| Policy | Behavior | +| --- | --- | +| `Every` | Start every action immediately and run them concurrently. It has no key. | +| `Restartable key` | Fence and cancel all running work for `key`, discard its queue, and start the new action. | +| `Drop key` | Ignore the new action while work for `key` is running or queued. | +| `Enqueue key` | Run every action for `key` in first-in, first-out order, one at a time. | +| `KeepLatest key` | Let the running action finish, retain only the newest pending action, and discard intermediate pending actions. | + +A realistic scheduler remains a small pattern match: ```purescript data Action - = LoadRemoteState - | PersistRemoteState - | ... + = SearchChanged String + | SaveClicked + | Autosave String + | UploadChunk Int Int + | RecordMetric String + +data Task + = SearchRequest + | SaveRequest + | AutosaveRequest + | Upload Int + +derive instance eqTask :: Eq Task +derive instance ordTask :: Ord Task + +schedule :: Action -> Halo.TaskPolicy Task +schedule = case _ of + SearchChanged _ -> Halo.Restartable SearchRequest + SaveClicked -> Halo.Drop SaveRequest + Autosave _ -> Halo.KeepLatest AutosaveRequest + UploadChunk fileId _ -> Halo.Enqueue (Upload fileId) + RecordMetric _ -> Halo.Every +``` -handleAction :: forall props state m. Action -> HaloM props state Action m Unit +Use one stable policy for a given key. Mixing policies on one key is defined by each arriving action, but is harder to reason about. -eval = Halo.mkEval Halo.defaultEval { initialize = Just LoadRemoteState, finalize = Just PersistRemoteState, handleAction = handleAction } -``` +`Every` can create unbounded concurrent work, and `Enqueue` can create an unbounded queue if producers are faster than consumers. Use `Drop` or `KeepLatest`, or bound input at its source, when load can spike. -`HaloM` is also a monad transformer, and so you can lift any monad `m` logic into `HaloM`. Just be aware that in order to run the logic, Halo requires that you `hoist` (convert) your chosen monad into `Aff` before returning it. +### Render activity -### Hoisting +`useHalo` and `component` return an `Activity key` snapshot. Activity changes trigger a React render. ```purescript -hoist :: forall props state action m m'. Functor m => (m ~> m') -> HaloM props state action m ~> HaloM props state action m' +let + search = Halo.activityFor SearchRequest activity + total = Halo.activityTotals activity + +in R.text $ + show search.running <> " search running, " <> + show total.queued <> " total queued" ``` -Example: +`activityFor` reports `{ running, queued }` for one keyed task. `activityTotals` includes all keyed work and unkeyed `Every` work. Lifecycle evaluations and structured child fibers are not included. + +## Lifecycle and cancellation + +The evaluator receives: ```purescript --- Inverting a reader -hoistReaderT :: - forall props state action env m. - HaloM props state action (ReaderT env m) ~> - ReaderT env (HaloM props state action m) -hoistReaderT x = do - env <- ask - lift (Halo.hoist (flip runReaderT env) x) +data Lifecycle props action + = Activate + | Update props -- previous props + | Action action ``` -### Working with props +`Activate` does **not** mean “exactly once.” React may run an effect setup, cleanup, and setup again for the same hook instance in development StrictMode. Halo treats each setup as a fresh active scope. Deactivation cancels that scope's action evaluations, queued work, lifecycle evaluations, structured children, and subscriptions; a later activation is usable again. + +`Update previousProps` runs when the props reference changes. Read current props with `Halo.props`. Halo keeps the latest evaluator, scheduler, error handler, and React update callbacks rather than permanently capturing the initial hook spec. + +There is no `Finalize` evaluator in v4. React cleanup is synchronous, so asynchronous finalizers would have misleading guarantees. Put external resources behind `subscribe` cleanup, an `Aff` bracket/finalizer, or another resource owner with explicit semantics. + +The task policy applies to dispatched actions, including actions emitted by subscriptions. `Activate` and `Update` evaluations are scope-owned but do not pass through `schedule`. If initialization should use a task policy, dispatch an ordinary action from the application boundary rather than hiding long-running work in lifecycle logic. + +### What cancellation guarantees + +Halo performs two operations on replacement or deactivation: + +1. It marks the old owner inactive immediately, blocking later Halo state commits and capability acquisition. +2. It requests cancellation of the owned `Aff` fibers. + +Cancellation cannot undo an HTTP request already sent, a log already written, or any other external effect already performed. Some foreign async APIs also cannot be interrupted. Model idempotency and server-side concurrency where correctness requires them; Halo's commit fence only protects the component's Halo state from stale work. + +`fork` creates a structured child of the current evaluation. The child is cancelled when its parent finishes, is replaced, or is deactivated. Use it only for concurrency within that evaluation, and use `kill` for earlier cancellation. Returning from the parent is not a way to create a detached component process. + +## Subscriptions + +Halo uses `Emitter` from `halogen-subscriptions`: ```purescript -props :: forall props action state m. HaloM props state action m props +Halo.Action StartListening -> do + subscriptionId <- Halo.subscribe eventEmitter + modify_ _ { subscriptionId = Just subscriptionId } + +Halo.Action StopListening -> do + { subscriptionId } <- get + traverse_ Halo.unsubscribe subscriptionId + modify_ _ { subscriptionId = Nothing } ``` -Example: +Manual `unsubscribe` removes the subscription from Halo's tracking. Any subscription still tracked at deactivation is unsubscribed automatically. New subscriptions from stale or inactive evaluations are rejected, and callbacks retained by a misbehaving source remain bound to their original scope rather than targeting a later reactivation. + +An `Emitter` is broadcast-style: every subscriber receives every emitted value. It is not a consuming work queue and provides no backpressure. Each event delivered to Halo is dispatched once and then follows its action policy. Halo v4 intentionally does not expose a coroutine, process, or saga API; task scheduling is the focused concurrency boundary. + +## Error handling + +Every spec must provide: ```purescript -fireOnChange :: - forall props state action m a. - MonadEffect m => - HaloM { onChange :: a -> Effect Unit | props } { value :: a | state } action m Unit -fireOnChange = do - { onChange } <- Halo.props - { value } <- Halo.get - liftEffect (onChange value) +onError :: Halo.ErrorContext props action -> Error -> Effect Unit ``` -### Working with state +The context is `ActivationError`, `UpdateError previousProps`, or `ActionError action`. Expected domain failures belong in the action/state model, usually by catching `Aff` errors inside `eval`. Unexpected uncaught errors go to `onError`. Cancellation caused by replacement or deactivation is suppressed rather than reported as an application failure. -`HaloM` doesn't have any special interface for reading and modifying state, instead providing an instance of [MonadState](https://pursuit.purescript.org/packages/purescript-transformers/docs/Control.Monad.State.Class) for flexibility. +## Component helper or hook -### Subscriptions - -Subscriptions registered using these functions are automatically tracked by Halo. +Use `Halo.component` when Halo owns the whole component. Its renderer receives: ```purescript -subscribe :: forall props state action m. Emitter action -> HaloM props state action m SubscriptionId +{ props :: props +, state :: state +, dispatch :: action -> Effect Unit +, activity :: Halo.Activity key +} +``` -unsubscribe :: forall props state action m. SubscriptionId -> HaloM props state action m Unit +Use `Halo.useHalo` when composing Halo with other React hooks: + +```purescript +halo <- Halo.useHalo + { props + , initialState + , eval + , schedule + , onError + } + +-- halo.state +-- halo.dispatch +-- halo.activity ``` -`Emitter` is from the `purescript-halogen-subscriptions` library. +`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use normal `get`, `put`, and `modify_`; use `liftAff` for asynchronous work. `Halo.props` reads the latest props. -There is also a version for subscriptions that want to unsubscribe themselves: +`mkEval` remains available for simple lifecycle-to-action routing: ```purescript -subscribe' :: forall props state action m. (SubscriptionId -> Emitter action) -> HaloM props state action m SubscriptionId +eval = Halo.mkEval $ Halo.defaultEval + { initialize = Just InitializeData + , handleAction = handleAction + } ``` -Any subscriptions that remain when the component is unmounted are automatically unsubscribed. This prevents requiring manual clean up in the `Finalize` lifecycle event. Also note that new subscriptions will not be created once the `Finalize` event has been fired. +The `update` field can map previous props to an optional action. These lifecycle-routed actions execute inside the lifecycle evaluation; they are not independently scheduled. -### Forking +## Migrating from v3 -Also provided are functions for creating and killing forks which launch processes in separate "threads" (or as useful an approximation as we can get in JavaScript): +Version 4 intentionally breaks the evaluator API to make cancellation and ownership reliable. -```purescript -fork :: forall props state action m. HaloM props state action m Unit -> HaloM props state action m ForkId +- Change `HaloM props state action m` to `HaloM props state action key`. Halo now runs directly on `Aff`; remove `hoist`, `HaloAp`, and the custom base monad parameter. +- Add an application task-key type with `Eq` and `Ord`, then add `schedule :: action -> TaskPolicy key`. +- Add `onError :: ErrorContext props action -> Error -> Effect Unit`. +- Replace `Initialize` with `Activate`. `Activate` is repeatable. +- Remove `Finalize` handlers. Use scoped cancellation, subscriptions, and `Aff` finalizers instead. +- Keep `Update previousProps`, and read current props with `Halo.props`. +- Replace the `useHalo` tuple with the record fields `state`, `dispatch`, and `activity`. +- In `component` renderers, rename `send` to `dispatch` and accept `activity` when needed. +- Revisit `fork`: v4 children are structured under the evaluation that created them, not detached until component unmount. +- Remove assumptions that action effects run without coordination. Choose `Every` explicitly for v3-like concurrent dispatch. -kill :: forall props state action m. ForkId -> HaloM props state action m Unit -``` +## Development -Similarly to subscriptions, when the component unmounts all still-running forks will be killed. However new forks _can_ be created during the `Finalize` phase but there is no way of killing them (as with Halogen). +Install the pinned tools and run the checks: + +```console +npm ci +npm run format:check +npm run build -- --strict +npm test +``` -### Parallelism +The runtime tests model React's effect setup-cleanup-setup sequence directly and use deterministic `AVar` gates for scheduling and cancellation. A DOM mounting test is intentionally omitted because this package's npm manifest contains only the PureScript compiler and Spago; the repeatable lifecycle contract is tested at the runtime boundary used by the hook. -Finally `HaloM` provides an instance of `Parallel` for converting back and forth between `HaloAp`, it's applicative counterpart. This allows any logic to be easily converted to run in `parallel` or `sequential`ly. +Module documentation is generated by PureScript and can be published to [Pursuit](https://pursuit.purescript.org/packages/purescript-react-halo) with a release. diff --git a/bower.json b/bower.json deleted file mode 100644 index 762a086..0000000 --- a/bower.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "purescript-react-halo", - "license": [ - "BSD-3-Clause" - ], - "repository": { - "type": "git", - "url": "https://github.com/robertdp/purescript-react-halo.git" - }, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "output" - ], - "dependencies": { - "purescript-aff": "^v7.0.0", - "purescript-bifunctors": "^v6.0.0", - "purescript-effect": "^v4.0.0", - "purescript-either": "^v6.1.0", - "purescript-foldable-traversable": "^v6.0.0", - "purescript-free": "^v7.0.0", - "purescript-freeap": "^v7.0.0", - "purescript-halogen-subscriptions": "https://github.com/purescript-halogen/purescript-halogen-subscriptions.git#v2.0.0", - "purescript-maybe": "^v6.0.0", - "purescript-newtype": "^v5.0.0", - "purescript-ordered-collections": "^v3.0.0", - "purescript-parallel": "^v6.0.0", - "purescript-prelude": "^v6.0.0", - "purescript-react-basic-hooks": "^v8.0.0", - "purescript-refs": "^v6.0.0", - "purescript-tailrec": "^v6.0.0", - "purescript-transformers": "^v6.0.0", - "purescript-tuples": "^v7.0.0", - "purescript-unsafe-reference": "^v5.0.0" - } -} diff --git a/package-lock.json b/package-lock.json index 197dde4..46d55d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,50 +1,123 @@ { "name": "purescript-react-halo", - "version": "1.0.0", - "lockfileVersion": 2, + "version": "4.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "purescript-react-halo", - "version": "1.0.0", - "license": "ISC", + "version": "4.0.0", + "license": "BSD-3-Clause", "devDependencies": { - "bower": "^1.8.14", - "pulp": "^16.0.2", - "purescript": "^0.15.4", - "purescript-psa": "^0.8.2", - "purs-tidy": "^0.9.0", - "rimraf": "^3.0.2", - "spago": "^0.20.9" + "purescript": "0.15.16", + "spago": "1.0.4" } }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-4.0.1.tgz", + "integrity": "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "4.0.0", + "run-parallel": "^1.2.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-4.0.0.tgz", + "integrity": "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-3.0.1.tgz", + "integrity": "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "4.0.1", + "fastq": "^1.15.0" + }, + "engines": { + "node": ">=18.18.0" + } }, "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "dev": true, + "license": "ISC", "dependencies": { - "@gar/promisify": "^1.0.1", + "@gar/promisify": "^1.1.3", "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", "dev": true, + "license": "MIT", "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" }, "engines": { - "node": ">=10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { @@ -52,6 +125,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -59,45 +133,31 @@ "node": ">=10" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, "bin": { - "acorn": "bin/acorn" + "rimraf": "bin.js" }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">= 10" } }, "node_modules/agent-base": { @@ -105,6 +165,7 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "4" }, @@ -113,13 +174,12 @@ } }, "node_modules/agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" }, "engines": { @@ -131,6 +191,7 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -139,38 +200,30 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "type-fest": "^0.21.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -178,6 +231,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -189,150 +243,49 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "license": "Python-2.0" }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", - "dev": true - }, - "node_modules/assert/node_modules/util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", - "dev": true, - "dependencies": { - "inherits": "2.0.1" - } - }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "license": "MIT" }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } @@ -341,535 +294,191 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/bower": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.14.tgz", - "integrity": "sha512-8Rq058FD91q9Nwthyhw0la9fzpBz0iwZTrt51LWl+w+PnJgZk9J+5wp3nibsJcIUPglMYXr4NRBaR+TUj0OkBQ==", "dev": true, - "bin": { - "bower": "bin/bower" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, - "node_modules/browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "JSONStream": "^1.0.3", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" + "fill-range": "^7.1.1" }, - "bin": { - "browser-pack": "bin/cmd.js" + "engines": { + "node": ">=8" } }, - "node_modules/browser-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "dependencies": { - "resolve": "^1.17.0" - } + "license": "MIT" }, - "node_modules/browserify": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.2.tgz", - "integrity": "sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g==", + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", "dev": true, - "dependencies": { - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^2.0.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.0", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^2.0.0", - "glob": "^7.1.0", - "has": "^1.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.0.0", - "JSONStream": "^1.0.3", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.2.3", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "~0.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^2.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.10.1", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "browserify": "bin/cmd.js" - }, + "optional": true, "engines": { - "node": ">= 0.8" + "node": ">=10.0.0" } }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, + "license": "MIT", "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/browserify-cache-api": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browserify-cache-api/-/browserify-cache-api-3.0.2.tgz", - "integrity": "sha512-14YNbboSgSHY5QNZSLwlGYB7OuBuXS7gMyR2gfBjdS4JYcWB9BqyKhraQG/VW2W5ZhjkC/C8LZ38sP3bmbmeNA==", + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", "dev": true, - "dependencies": { - "async": "^2.6.4", - "through2": "^2.0.0", - "xtend": "^4.0.0" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "node_modules/cacache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", + "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", "dev": true, + "license": "ISC", "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" } }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/browserify-incremental": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/browserify-incremental/-/browserify-incremental-3.1.1.tgz", - "integrity": "sha512-PrFwOzLEdy27VRXK2uGjmjLq1aROBG7QoQq3eKanmm6Q8vuzT0ZNFCORHh3yJgNQQooXA9tOizGv4vCOmhrvRQ==", + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, - "dependencies": { - "browserify-cache-api": "^3.0.0", - "JSONStream": "^0.10.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "browserifyinc": "bin/cmd.js" - }, - "peerDependencies": { - "browserify": "*" - } + "license": "ISC" }, - "node_modules/browserify-incremental/node_modules/jsonparse": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", - "integrity": "sha512-fw7Q/8gFR8iSekUi9I+HqWIap6mywuoe7hQIg3buTVjuZgALKj4HAmm0X6f+TaL4c9NJbvyFQdaI2ppr5p6dnQ==", + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, - "engines": [ - "node >= 0.2.0" - ] + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "node_modules/browserify-incremental/node_modules/JSONStream": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", - "integrity": "sha512-8XbSFFd43EG+1thjLNFIzCBlwXti0yKa7L+ak/f0T/pkC+31b7G41DXL/JzYpAoYWZ2eCPiu4IIqzijM8N0a/w==", + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, + "license": "MIT", "dependencies": { - "jsonparse": "0.0.5", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "index.js" + "restore-cursor": "^3.1.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserify/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true - }, - "node_modules/byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cacache": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", - "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", - "dev": true, - "dependencies": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "node_modules/cacache/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/cached-path-relative": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", - "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", - "dev": true - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" + "color-name": "1.1.3" } }, "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==", - "dev": true, - "dependencies": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "engines": [ - "node >= 6.0" + "node >= 0.8" ], + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^3.0.2", + "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", - "dev": true - }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "deprecated": "This package is no longer supported.", "dev": true, + "license": "ISC", "dependencies": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", @@ -879,72 +488,34 @@ "run-queue": "^1.0.0" } }, - "node_modules/copy-concurrently/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } + "license": "MIT" }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", "dev": true, + "hasInstallScript": true, + "optional": true, "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -959,6 +530,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -969,59 +541,21 @@ "node": ">= 8" } }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, "node_modules/cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==", - "dev": true - }, - "node_modules/dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", - "dev": true - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", + "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==", "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } + "license": "MIT" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1032,187 +566,109 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, "engines": { - "node": ">=0.4.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "dependencies": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" + "license": "MIT", + "engines": { + "node": ">=12" }, - "bin": { - "deps-sort": "bin/cmd.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, + "license": "MIT", "dependencies": { + "end-of-stream": "^1.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true, - "engines": { - "node": ">=0.4", - "npm": ">=1.2" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "license": "MIT" }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1221,47 +677,25 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, - "node_modules/events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, "node_modules/execa": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", @@ -1277,85 +711,86 @@ "node": "^8.12.0 || >=9.7.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true + "deprecated": "This module is no longer supported.", + "dev": true, + "license": "ISC" }, "node_modules/filesize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz", "integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 0.4.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, "engines": { - "node": "*" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, + "license": "ISC", "engines": { - "node": ">= 0.12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/from2": { @@ -1363,35 +798,47 @@ "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, - "node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, + "license": "MIT", "dependencies": { - "minipass": "^2.6.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" } }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, + "license": "ISC", "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", + "deprecated": "This package is no longer supported.", "dev": true, + "license": "ISC", "dependencies": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", @@ -1403,37 +850,29 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "node_modules/fuse.js": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.5.0.tgz", + "integrity": "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==", "dev": true, - "dependencies": { - "globule": "^1.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 4.0.0" + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" } }, - "node_modules/get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true - }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -1444,20 +883,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1473,173 +905,38 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globule": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "dev": true, - "dependencies": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/globule/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globule/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } + "license": "ISC" }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, + "license": "MIT", "dependencies": { - "@tootallnate/once": "1", + "@tootallnate/once": "2", "agent-base": "6", "debug": "4" }, @@ -1647,32 +944,12 @@ "node": ">= 6" } }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "6", "debug": "4" @@ -1686,6 +963,7 @@ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.0.0" } @@ -1695,6 +973,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -1703,37 +982,19 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -1743,6 +1004,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1751,13 +1013,16 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1767,97 +1032,87 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha512-0mVWSSbNDvedDWIN4wxLsdPM4a7cIPcpyMxj3QZ406QRwQ6ePGB1YIHxVPjqpcUGbWQ5C+nHTwGNWAGvt7ggVA==", + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", "dev": true, - "dependencies": { - "source-map": "~0.5.3" + "license": "MIT", + "engines": { + "node": ">= 12" } }, - "node_modules/insert-module-globals": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", - "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, - "dependencies": { - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "JSONStream": "^1.0.3", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - }, + "license": "MIT", "bin": { - "insert-module-globals": "bin/cmd.js" + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/insert-module-globals/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, "engines": { - "node": ">=4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1867,6 +1122,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -1874,139 +1130,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha512-nKtD/Qxm7tWdZqJoldEC7fF0S41v0mWbeaXG3637stOWfyGxTgWTYE2wtfKmjzpvxv2MA2xzxsXOIiwUpkX6Qw==", "dev": true, - "dependencies": { - "jsonify": "~0.0.0" - } - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "node_modules/jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] + "license": "ISC" }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" + "@isaacs/cliui": "^9.0.0" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, + "license": "MIT", "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" + "universalify": "^2.0.0" }, - "engines": { - "node": ">=0.6.0" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" + "uc.micro": "^2.0.0" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", - "dev": true - }, "node_modules/log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^2.4.2" }, @@ -2015,17 +1223,19 @@ } }, "node_modules/log-update": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-3.4.0.tgz", - "integrity": "sha512-ILKe88NeMt4gmDvk/eb615U/IVn7K9KWGkoYbdatQ69Z65nj1ZzjM6fHXfcs0Uge+e+EGnMW7DY4T9yko8vWFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-escapes": "^3.2.0", - "cli-cursor": "^2.1.0", - "wrap-ansi": "^5.0.0" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2036,64 +1246,77 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "dev": true, + "license": "ISC", "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", + "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", + "negotiator": "^0.6.3", "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" }, "engines": { - "node": ">= 10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/make-fetch-happen/node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" }, "engines": { - "node": ">= 10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/make-fetch-happen/node_modules/chownr": { @@ -2101,29 +1324,50 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/make-fetch-happen/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "node_modules/make-fetch-happen/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" @@ -2134,6 +1378,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -2141,108 +1386,175 @@ "node": ">=10" } }, - "node_modules/make-fetch-happen/node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "node_modules/make-fetch-happen/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { - "minipass": "^3.1.1" + "glob": "^7.1.3" }, - "engines": { - "node": ">= 8" + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/make-fetch-happen/node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "node_modules/make-fetch-happen/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, + "license": "MIT", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/make-fetch-happen/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">= 10" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "node_modules/make-fetch-happen/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", "dev": true, + "license": "ISC", "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" + "minipass": "^3.1.1" }, - "bin": { - "miller-rabin": "bin/miller-rabin" + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", "dev": true, - "bin": { - "mime": "cli.js" + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" }, "engines": { - "node": ">=4" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, "engines": { - "node": ">= 0.6" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", + "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/mimic-fn": { @@ -2250,27 +1562,17 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2279,16 +1581,21 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -2301,6 +1608,7 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -2309,27 +1617,29 @@ } }, "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "dev": true, + "license": "MIT", "dependencies": { - "minipass": "^3.1.0", + "minipass": "^3.1.6", "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" + "minizlib": "^2.1.2" }, "engines": { - "node": ">=8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" }, "optionalDependencies": { - "encoding": "^0.1.12" + "encoding": "^0.1.13" } }, "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "minipass": "^3.0.0" }, @@ -2342,6 +1652,7 @@ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -2354,6 +1665,7 @@ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -2365,13 +1677,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, + "license": "MIT", "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -2384,13 +1698,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", @@ -2407,26 +1723,12 @@ "node": ">=4.0.0" } }, - "node_modules/mississippi/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -2434,77 +1736,13 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "node_modules/module-deps": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", - "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", - "dev": true, - "dependencies": { - "browser-resolve": "^2.0.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "JSONStream": "^1.0.3", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "module-deps": "bin/cmd.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/module-deps/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/mold-source-map": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/mold-source-map/-/mold-source-map-0.4.0.tgz", - "integrity": "sha512-Y0uA/sDKVuPgLd7BmaJOai+fqzjrOlR6vZgx5cJIvturI/xOPQPgbf3X7ZbzJd6MvqQ6ucIfK8dSteFyc2Mw2w==", - "dev": true, - "dependencies": { - "convert-source-map": "^1.1.0", - "through": "~2.2.7" - } - }, - "node_modules/mold-source-map/node_modules/through": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", - "integrity": "sha512-JIR0m0ybkmTcR8URann+HbwKmodP+OE8UCbsifQDYMLD5J3em1Cdn3MYPpbEd5elGDwmP98T+WbqP/tvzA5Mjg==", - "dev": true - }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", + "deprecated": "This package is no longer supported.", "dev": true, + "license": "ISC", "dependencies": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", @@ -2514,61 +1752,37 @@ "run-queue": "^1.0.3" } }, - "node_modules/move-concurrently/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT", + "optional": true }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/node-static": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", - "integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==", - "dev": true, - "dependencies": { - "colors": ">=0.6.0", - "mime": "^1.2.9", - "optimist": ">=0.3.4" - }, - "bin": { - "static": "bin/cli.js" - }, - "engines": { - "node": ">= 0.4.1" - } - }, "node_modules/npm-run-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -2576,29 +1790,12 @@ "node": ">=8" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -2608,6 +1805,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -2618,42 +1816,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==", + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, + "license": "MIT", "dependencies": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "node_modules/optimist/node_modules/minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==", - "dev": true - }, - "node_modules/optimist/node_modules/wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "dev": true, + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, "engines": { - "node": ">=0.4.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true - }, "node_modules/p-finally": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2663,6 +1850,7 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -2673,56 +1861,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "dev": true, + "license": "MIT", "dependencies": { "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } }, - "node_modules/parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==", - "dev": true, - "dependencies": { - "path-platform": "~0.11.15" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2732,73 +1895,81 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, "engines": { - "node": ">= 0.8.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.12" + "node": "20 || >=22" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.6.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, + "license": "MIT", "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -2807,65 +1978,12 @@ "node": ">=10" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/pulp": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/pulp/-/pulp-16.0.2.tgz", - "integrity": "sha512-dvLRBMP2q1hgsHm9xRGskWJdx/Q3fDugLEAlT1zJIKRP7x/EWirMDi31jAWmM7oLkKC+3EedJ7i9d9NHeSb87A==", - "dev": true, - "dependencies": { - "browserify": "^16.2.3", - "browserify-incremental": "^3.1.1", - "concat-stream": "^2.0.0", - "gaze": "^1.1.3", - "glob": "^7.1.3", - "mold-source-map": "^0.4.0", - "node-static": "^0.7.11", - "read": "^1.0.7", - "sorcery": "^0.10.0", - "temp": "^0.9.0", - "through": "^2.3.8", - "tree-kill": "^1.2.1", - "which": "^1.3.1", - "wordwrap": "1.0.0" - }, - "bin": { - "pulp": "index.js" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -2876,6 +1994,7 @@ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, + "license": "MIT", "dependencies": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -2887,37 +2006,53 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } }, "node_modules/purescript": { - "version": "0.15.4", - "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.15.4.tgz", - "integrity": "sha512-6Ge3IMcIxKkOUXg91cBgvjbqu5SxdfwbvWi1P4g+E2maxfvDv+roWAmLyOteTsxQE4SEa/wWoCZvnZ/AEjvrMw==", + "version": "0.15.16", + "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.15.16.tgz", + "integrity": "sha512-3t936C7oUl/DT2n0U9SXO7TBG316EqywCUUuZ5naS9EhNc+Xpj+CUTjfNjlWffvN8oFDSWYdQGRFGJ73lXpCSQ==", "dev": true, "hasInstallScript": true, + "license": "ISC", "dependencies": { - "purescript-installer": "^0.2.6" + "purescript-installer": "^0.3.5" }, "bin": { "purs": "purs.bin" } }, "node_modules/purescript-installer": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.2.6.tgz", - "integrity": "sha512-22un7k/S0hETctsMMVlCEuXlvy1VAgN/uL49B65dQVPUVd17fh9CbB5kd9xoqiADUyw3t5HgL68lCwmDwrCCVw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.3.5.tgz", + "integrity": "sha512-w04BBvW4BSQlspLsZ9Bs5vtUBZqvC4bC+FizC5GUp2/rpmYvXJ0CTf79Q3MKo2A8p1ZjqbhCI+SqdTQ30UZUIQ==", "dev": true, + "license": "ISC", "dependencies": { - "arch": "^2.1.1", "byline": "^5.0.0", "cacache": "^11.3.2", "chalk": "^2.4.2", @@ -2926,16 +2061,14 @@ "filesize": "^4.1.2", "is-plain-obj": "^2.0.0", "log-symbols": "^3.0.0", - "log-update": "^3.2.0", + "log-update": "^4.0.0", + "make-fetch-happen": "^10.0.0", "minimist": "^1.2.0", - "mkdirp": "^0.5.1", "ms": "^2.1.2", "once": "^1.4.0", - "pump": "^3.0.0", - "request": "^2.88.0", "rimraf": "^2.6.3", "semver": "^7.3.7", - "tar": "^4.4.6", + "tar": "^6.1.11", "which": "^1.3.1", "zen-observable": "^0.8.14" }, @@ -2943,321 +2076,172 @@ "install-purescript": "index.js" }, "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/purescript-installer/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "node": ">=12" } }, - "node_modules/purescript-psa": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/purescript-psa/-/purescript-psa-0.8.2.tgz", - "integrity": "sha512-4Olf0aQQrNCfcDLXQI3gJgINEQ+3U+4QPLmQ2LHX2L/YOXSwM7fOGIUs/wMm/FQnwERUyQmHKQTJKB4LIjE2fg==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "bin": { - "psa": "index.js" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/purs-tidy": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/purs-tidy/-/purs-tidy-0.9.0.tgz", - "integrity": "sha512-7la7Jw5CyuMDXJMliGiK746xevUScDCTJ0eMvN/mV/NAQF2c9Cqa31QNx5O15ae2d7SuFDqEiVALFzHYD601Cg==", + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "bin": { - "purs-tidy": "bin/index.js" + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">= 0.8.0" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, "engines": { - "node": ">=0.4.x" + "node": ">=8" } }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.4.x" + "node": ">= 4" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" } }, - "node_modules/read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, - "dependencies": { - "mute-stream": "~0.0.4" - }, + "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha512-3ALe0bjBVZtkdWKIcThYpQCLbBMd/+Tbh2CDSrAIDO3UsZ4Xs+tnyjv2MjCOMMgBG+AsUOeuP1cgtY1INISc8w==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", "dependencies": { - "readable-stream": "^2.0.2" + "queue-microtask": "^1.2.2" } }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", "dev": true, + "license": "ISC", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "aproba": "^1.1.1" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { + "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/readable-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", - "dev": true, - "dependencies": { - "aproba": "^1.1.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "dev": true, + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "node_modules/sander/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } + "license": "MIT" }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3265,61 +2249,12 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha512-UTzHm/+AzKfO9RgPgRpDIuMSNie1ubXRaljjlhFMNGYoG7z+rm9AHLPMf70R7887xboDH9Q+5YQbWKObFHEAtw==", - "dev": true, - "dependencies": { - "json-stable-stringify": "~0.0.0", - "sha.js": "~2.4.4" - } - }, - "node_modules/shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "dev": true, - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3332,71 +2267,104 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", - "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, + "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -3406,4051 +2374,629 @@ "node": ">= 10" } }, - "node_modules/sorcery": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", - "integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==", - "dev": true, - "dependencies": { - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0", - "sourcemap-codec": "^1.3.0" + "node_modules/spago": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spago/-/spago-1.0.4.tgz", + "integrity": "sha512-iv1HBJppzGJRWWd/DZ32oselcKrhq/PboBdXy5VMX4GiwRqLCUUp3loZ6Yhm8zymNRKHkN/w52cxX6k521nwng==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@nodelib/fs.walk": "^3.0.1", + "env-paths": "^3.0.0", + "fs-extra": "^11.3.0", + "fuse.js": "^7.1.0", + "glob": "^11.0.1", + "markdown-it": "^14.1.0", + "micromatch": "^4.0.8", + "open": "^10.1.0", + "picomatch": "^4.0.2", + "punycode": "^2.3.1", + "readline-sync": "^1.4.10", + "semver": "^7.7.1", + "spdx-expression-parse": "^4.0.0", + "ssh2": "^1.16.0", + "supports-color": "^10.0.0", + "tar": "^7.4.3", + "tmp": "^0.2.3", + "xhr2": "^0.2.1", + "yaml": "^2.7.0" }, "bin": { - "sorcery": "bin/index.js" + "spago": "bin/bundle.js" + }, + "engines": { + "node": ">=22.5.0" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "node_modules/spago/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "18 || 20 || >=22" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, - "node_modules/spago": { - "version": "0.20.9", - "resolved": "https://registry.npmjs.org/spago/-/spago-0.20.9.tgz", - "integrity": "sha512-r5TUxnYn9HawlQyMswlhIk24BGFSN2KGbqgZFZrn47GjTpMscU14xkt9CqTWgoSQYsoZieG+3dUtOxUQ7GYD7w==", + "node_modules/spago/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "dependencies": { - "make-fetch-happen": "^9.1.0", - "tar": "^6.1.11" + "balanced-match": "^4.0.2" }, - "bin": { - "spago": "spago" + "engines": { + "node": "20 || >=22" } }, "node_modules/spago/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/spago/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "node_modules/spago/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">= 8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/spago/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/spago/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, "bin": { - "mkdirp": "bin/cmd.js" + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/spago/node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "node_modules/spago/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">= 10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/spago/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "node_modules/spago/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "node_modules/spago/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, + "license": "MIT", "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" + "minipass": "^7.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 18" } }, - "node_modules/ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", - "dev": true, - "dependencies": { - "figgy-pudding": "^3.5.1" - } - }, - "node_modules/stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "dependencies": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", - "dev": true, - "dependencies": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-http/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "node_modules/stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", - "dev": true, - "dependencies": { - "minimist": "^1.1.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/spago/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "dev": true, - "dependencies": { - "acorn-node": "^1.2.0" - } - }, - "node_modules/tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "dev": true, - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/tar/node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha512-PIxwAupJZiYU4JmVZYwXp9FKsHMXb5h0ZEFyuXTAn8WLHOlcij+FEcbrvDsom1o5dr1YggEtFbECvGCW2sT53Q==", - "dev": true, - "dependencies": { - "process": "~0.11.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "node_modules/umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", - "dev": true, - "bin": { - "umd": "bin/cli.js" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "node_modules/spago/node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, - "bin": { - "undeclared-identifiers": "bin.js" - } - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true - }, - "node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/util/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", - "dev": true - } - }, - "dependencies": { - "@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true - }, - "@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "dev": true, - "requires": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "dev": true, - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "depd": "^1.1.2", - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "dev": true - }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true - }, - "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "bower": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.14.tgz", - "integrity": "sha512-8Rq058FD91q9Nwthyhw0la9fzpBz0iwZTrt51LWl+w+PnJgZk9J+5wp3nibsJcIUPglMYXr4NRBaR+TUj0OkBQ==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, - "browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", - "dev": true, - "requires": { - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "JSONStream": "^1.0.3", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" - } - }, - "browser-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", - "dev": true, - "requires": { - "resolve": "^1.17.0" - } - }, - "browserify": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.2.tgz", - "integrity": "sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g==", - "dev": true, - "requires": { - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^2.0.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.0", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^2.0.0", - "glob": "^7.1.0", - "has": "^1.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.0.0", - "JSONStream": "^1.0.3", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.2.3", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "~0.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^2.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.10.1", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - } - } - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cache-api": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browserify-cache-api/-/browserify-cache-api-3.0.2.tgz", - "integrity": "sha512-14YNbboSgSHY5QNZSLwlGYB7OuBuXS7gMyR2gfBjdS4JYcWB9BqyKhraQG/VW2W5ZhjkC/C8LZ38sP3bmbmeNA==", - "dev": true, - "requires": { - "async": "^2.6.4", - "through2": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-incremental": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/browserify-incremental/-/browserify-incremental-3.1.1.tgz", - "integrity": "sha512-PrFwOzLEdy27VRXK2uGjmjLq1aROBG7QoQq3eKanmm6Q8vuzT0ZNFCORHh3yJgNQQooXA9tOizGv4vCOmhrvRQ==", - "dev": true, - "requires": { - "browserify-cache-api": "^3.0.0", - "JSONStream": "^0.10.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "jsonparse": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", - "integrity": "sha512-fw7Q/8gFR8iSekUi9I+HqWIap6mywuoe7hQIg3buTVjuZgALKj4HAmm0X6f+TaL4c9NJbvyFQdaI2ppr5p6dnQ==", - "dev": true - }, - "JSONStream": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", - "integrity": "sha512-8XbSFFd43EG+1thjLNFIzCBlwXti0yKa7L+ak/f0T/pkC+31b7G41DXL/JzYpAoYWZ2eCPiu4IIqzijM8N0a/w==", - "dev": true, - "requires": { - "jsonparse": "0.0.5", - "through": ">=2.2.7 <3" - } - } - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true - }, - "byline": { + "node_modules/spago/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", - "dev": true - }, - "cacache": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", - "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "cached-path-relative": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", - "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true - }, - "combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==", - "dev": true, - "requires": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true - }, - "convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==", - "dev": true - }, - "dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", - "dev": true, - "requires": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" - } - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "requires": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - } - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true - }, - "err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", - "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^3.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true - }, - "filesize": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz", - "integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "requires": { - "minipass": "^2.6.0" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globule": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==", - "dev": true - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha512-0mVWSSbNDvedDWIN4wxLsdPM4a7cIPcpyMxj3QZ406QRwQ6ePGB1YIHxVPjqpcUGbWQ5C+nHTwGNWAGvt7ggVA==", - "dev": true, - "requires": { - "source-map": "~0.5.3" - } - }, - "insert-module-globals": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", - "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", - "dev": true, - "requires": { - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "JSONStream": "^1.0.3", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - }, - "dependencies": { - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - } - } - }, - "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha512-nKtD/Qxm7tWdZqJoldEC7fF0S41v0mWbeaXG3637stOWfyGxTgWTYE2wtfKmjzpvxv2MA2xzxsXOIiwUpkX6Qw==", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA==", - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", - "dev": true - }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2" - } - }, - "log-update": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-3.4.0.tgz", - "integrity": "sha512-ILKe88NeMt4gmDvk/eb615U/IVn7K9KWGkoYbdatQ69Z65nj1ZzjM6fHXfcs0Uge+e+EGnMW7DY4T9yko8vWFg==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "cli-cursor": "^2.1.0", - "wrap-ansi": "^5.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dev": true, - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "dependencies": { - "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "dev": true, - "requires": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "dev": true, - "requires": { - "minipass": "^3.1.1" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "dev": true, - "requires": { - "encoding": "^0.1.12", - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - }, - "dependencies": { - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - } - } - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "module-deps": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", - "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", - "dev": true, - "requires": { - "browser-resolve": "^2.0.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "JSONStream": "^1.0.3", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - } - } - }, - "mold-source-map": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/mold-source-map/-/mold-source-map-0.4.0.tgz", - "integrity": "sha512-Y0uA/sDKVuPgLd7BmaJOai+fqzjrOlR6vZgx5cJIvturI/xOPQPgbf3X7ZbzJd6MvqQ6ucIfK8dSteFyc2Mw2w==", - "dev": true, - "requires": { - "convert-source-map": "^1.1.0", - "through": "~2.2.7" - }, - "dependencies": { - "through": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", - "integrity": "sha512-JIR0m0ybkmTcR8URann+HbwKmodP+OE8UCbsifQDYMLD5J3em1Cdn3MYPpbEd5elGDwmP98T+WbqP/tvzA5Mjg==", - "dev": true - } - } - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - }, - "node-static": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", - "integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==", - "dev": true, - "requires": { - "colors": ">=0.6.0", - "mime": "^1.2.9", - "optimist": ">=0.3.4" - } - }, - "npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "dev": true - } - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==", - "dev": true, - "requires": { - "path-platform": "~0.11.15" - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==", - "dev": true - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "requires": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - } - }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "pulp": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/pulp/-/pulp-16.0.2.tgz", - "integrity": "sha512-dvLRBMP2q1hgsHm9xRGskWJdx/Q3fDugLEAlT1zJIKRP7x/EWirMDi31jAWmM7oLkKC+3EedJ7i9d9NHeSb87A==", - "dev": true, - "requires": { - "browserify": "^16.2.3", - "browserify-incremental": "^3.1.1", - "concat-stream": "^2.0.0", - "gaze": "^1.1.3", - "glob": "^7.1.3", - "mold-source-map": "^0.4.0", - "node-static": "^0.7.11", - "read": "^1.0.7", - "sorcery": "^0.10.0", - "temp": "^0.9.0", - "through": "^2.3.8", - "tree-kill": "^1.2.1", - "which": "^1.3.1", - "wordwrap": "1.0.0" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - }, - "purescript": { - "version": "0.15.4", - "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.15.4.tgz", - "integrity": "sha512-6Ge3IMcIxKkOUXg91cBgvjbqu5SxdfwbvWi1P4g+E2maxfvDv+roWAmLyOteTsxQE4SEa/wWoCZvnZ/AEjvrMw==", - "dev": true, - "requires": { - "purescript-installer": "^0.2.6" - } - }, - "purescript-installer": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.2.6.tgz", - "integrity": "sha512-22un7k/S0hETctsMMVlCEuXlvy1VAgN/uL49B65dQVPUVd17fh9CbB5kd9xoqiADUyw3t5HgL68lCwmDwrCCVw==", - "dev": true, - "requires": { - "arch": "^2.1.1", - "byline": "^5.0.0", - "cacache": "^11.3.2", - "chalk": "^2.4.2", - "env-paths": "^2.2.0", - "execa": "^2.0.3", - "filesize": "^4.1.2", - "is-plain-obj": "^2.0.0", - "log-symbols": "^3.0.0", - "log-update": "^3.2.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "ms": "^2.1.2", - "once": "^1.4.0", - "pump": "^3.0.0", - "request": "^2.88.0", - "rimraf": "^2.6.3", - "semver": "^7.3.7", - "tar": "^4.4.6", - "which": "^1.3.1", - "zen-observable": "^0.8.14" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "purescript-psa": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/purescript-psa/-/purescript-psa-0.8.2.tgz", - "integrity": "sha512-4Olf0aQQrNCfcDLXQI3gJgINEQ+3U+4QPLmQ2LHX2L/YOXSwM7fOGIUs/wMm/FQnwERUyQmHKQTJKB4LIjE2fg==", - "dev": true - }, - "purs-tidy": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/purs-tidy/-/purs-tidy-0.9.0.tgz", - "integrity": "sha512-7la7Jw5CyuMDXJMliGiK746xevUScDCTJ0eMvN/mV/NAQF2c9Cqa31QNx5O15ae2d7SuFDqEiVALFzHYD601Cg==", - "dev": true - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", - "dev": true, - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha512-3ALe0bjBVZtkdWKIcThYpQCLbBMd/+Tbh2CDSrAIDO3UsZ4Xs+tnyjv2MjCOMMgBG+AsUOeuP1cgtY1INISc8w==", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "dependencies": { - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - } - } - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "requires": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha512-UTzHm/+AzKfO9RgPgRpDIuMSNie1ubXRaljjlhFMNGYoG7z+rm9AHLPMf70R7887xboDH9Q+5YQbWKObFHEAtw==", - "dev": true, - "requires": { - "json-stable-stringify": "~0.0.0", - "sha.js": "~2.4.4" - } - }, - "shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "dev": true, - "requires": { - "fast-safe-stringify": "^2.0.7" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true - }, - "socks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", - "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "requires": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, - "socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - } + "license": "CC-BY-3.0" }, - "sorcery": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", - "integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==", + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, - "requires": { - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0", - "sourcemap-codec": "^1.3.0" + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" }, - "spago": { - "version": "0.20.9", - "resolved": "https://registry.npmjs.org/spago/-/spago-0.20.9.tgz", - "integrity": "sha512-r5TUxnYn9HawlQyMswlhIk24BGFSN2KGbqgZFZrn47GjTpMscU14xkt9CqTWgoSQYsoZieG+3dUtOxUQ7GYD7w==", + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", "dev": true, - "requires": { - "make-fetch-happen": "^9.1.0", - "tar": "^6.1.11" - }, + "hasInstallScript": true, "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" } }, - "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { + "node_modules/ssri": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "figgy-pudding": "^3.5.1" } }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", - "dev": true, - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { + "node_modules/stream-each": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" } }, - "stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } + "license": "MIT" }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "requires": { - "safe-buffer": "~5.2.0" + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { - "ansi-regex": "^4.1.0" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-final-newline": { + "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", "dev": true, - "requires": { - "minimist": "^1.1.0" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "supports-color": { + "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } }, - "syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, - "requires": { - "acorn-node": "^1.2.0" + "license": "ISC", + "engines": { + "node": ">=10" } }, - "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "requires": { - "minipass": "^2.9.0" - } - } + "license": "ISC", + "engines": { + "node": ">=8" } }, - "temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, - "requires": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "engines": { + "node": ">=10" } }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" }, - "through2": { + "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha512-PIxwAupJZiYU4JmVZYwXp9FKsHMXb5h0ZEFyuXTAn8WLHOlcij+FEcbrvDsom1o5dr1YggEtFbECvGCW2sT53Q==", + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, - "requires": { - "process": "~0.11.0" + "license": "MIT", + "engines": { + "node": ">=14.14" } }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, + "license": "MIT", "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - } - } - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, - "tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "tweetnacl": { + "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "typedarray": { + "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", - "dev": true + "dev": true, + "license": "MIT" }, - "undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, - "requires": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" - } + "license": "MIT" }, - "unique-filename": { + "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "unique-slug": "^2.0.0" } }, - "unique-slug": { + "node_modules/unique-slug": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "imurmurhash": "^0.1.4" } }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">= 10.0.0" } }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, + "license": "MIT" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true - } + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "requires": { - "inherits": "2.0.3" - }, + "license": "MIT", "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - } + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, + "license": "MIT", "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - } + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "isexe": "^2.0.0" + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } + "license": "MIT" }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } }, - "xtend": { + "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } }, - "y18n": { + "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "yallist": { + "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } }, - "zen-observable": { + "node_modules/zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", - "dev": true + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 83e12a8..062f352 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,20 @@ { "name": "purescript-react-halo", - "version": "1.0.0", - "description": "", - "main": "index.js", + "version": "4.0.0", + "description": "Race-safe action and effect scheduling for PureScript React components", "scripts": { "build": "spago build", - "docs": "rimraf ./output/React.Halo.* && spago docs --format markdown", - "postinstall": "spago install", - "test": "spago test --config ./spago.test.dhall" + "format": "npx --yes purs-tidy@0.11.1 format-in-place src test", + "format:check": "npx --yes purs-tidy@0.11.1 check src test", + "test": "spago test" }, - "author": "", - "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/robertdp/purescript-react-halo.git" + }, + "license": "BSD-3-Clause", "devDependencies": { - "bower": "^1.8.14", - "pulp": "^16.0.2", - "purescript": "^0.15.4", - "purescript-psa": "^0.8.2", - "purs-tidy": "^0.9.0", - "rimraf": "^3.0.2", - "spago": "^0.20.9" + "purescript": "0.15.16", + "spago": "1.0.4" } } diff --git a/packages.dhall b/packages.dhall deleted file mode 100644 index 30d8b94..0000000 --- a/packages.dhall +++ /dev/null @@ -1,5 +0,0 @@ -let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20220718/packages.dhall - sha256:a6d66723b6109f1e3eaf6575910f1c51aa545965ce313024ba329360e2f009ac - -in upstream diff --git a/spago.dhall b/spago.dhall deleted file mode 100644 index e574b52..0000000 --- a/spago.dhall +++ /dev/null @@ -1,27 +0,0 @@ -{ name = "react-halo" -, license = "BSD-3-Clause" -, repository = "https://github.com/robertdp/purescript-react-halo.git" -, dependencies = - [ "aff" - , "bifunctors" - , "effect" - , "either" - , "foldable-traversable" - , "free" - , "freeap" - , "halogen-subscriptions" - , "maybe" - , "newtype" - , "ordered-collections" - , "parallel" - , "prelude" - , "react-basic-hooks" - , "refs" - , "tailrec" - , "transformers" - , "tuples" - , "unsafe-reference" - ] -, packages = ./packages.dhall -, sources = [ "src/**/*.purs" ] -} diff --git a/spago.lock b/spago.lock new file mode 100644 index 0000000..9d0570a --- /dev/null +++ b/spago.lock @@ -0,0 +1,1940 @@ +{ + "workspace": { + "packages": { + "react-halo": { + "path": "./", + "core": { + "dependencies": [ + "aff", + "arrays", + "avar", + "effect", + "either", + "foldable-traversable", + "halogen-subscriptions", + "maybe", + "newtype", + "ordered-collections", + "prelude", + "react-basic-hooks", + "refs", + "transformers", + "tuples", + "unsafe-reference" + ] + }, + "test": { + "dependencies": [ + "console", + "control", + "exceptions", + "parallel", + "react-basic-dom", + "spec", + "spec-node" + ] + } + } + }, + "package_set": { + "address": { + "registry": "80.8.0" + }, + "compiler": ">=0.15.15 <0.16.0", + "content": { + "abc-parser": "2.1.0", + "ace": "9.1.0", + "address-rfc2821": "0.1.1", + "aff": "8.0.0", + "aff-bus": "6.0.0", + "aff-coroutines": "9.0.0", + "aff-promise": "4.0.0", + "aff-retry": "2.0.0", + "affjax": "13.0.0", + "affjax-node": "1.0.0", + "affjax-web": "1.0.0", + "ansi": "7.0.0", + "apexcharts": "0.5.0", + "applicative-phases": "1.0.0", + "argonaut": "9.0.0", + "argonaut-aeson-generic": "0.4.1", + "argonaut-codecs": "9.1.0", + "argonaut-core": "7.0.0", + "argonaut-generic": "8.0.0", + "argonaut-traversals": "10.0.0", + "argparse-basic": "2.0.0", + "array-builder": "0.1.2", + "array-search": "0.6.0", + "arraybuffer": "13.2.0", + "arraybuffer-builder": "3.1.0", + "arraybuffer-types": "3.0.2", + "arrays": "7.3.0", + "arrays-extra": "0.6.2", + "arrays-zipper": "2.0.1", + "ask": "1.0.0", + "assert": "6.0.0", + "assert-multiple": "0.4.0", + "avar": "5.0.1", + "axon": "0.0.3", + "b64": "0.0.9", + "barbies": "1.0.1", + "barlow-lens": "1.0.0", + "baskerville-core": "0.1.3", + "baskerville-mycroft": "0.1.0", + "benchlib": "0.0.4", + "bifunctors": "6.1.0", + "bigints": "7.0.1", + "blessed": "1.0.0", + "bolson": "0.3.9", + "bookhound": "0.1.7", + "bower-json": "3.0.0", + "bytestrings": "9.0.0", + "call-by-name": "4.0.1", + "canvas": "6.0.0", + "canvas-action": "9.0.0", + "cartesian": "1.0.6", + "catenable-lists": "7.0.0", + "cbor-stream": "1.3.0", + "chameleon": "1.0.0", + "chameleon-halogen": "1.0.3", + "chameleon-react-basic": "1.1.0", + "chameleon-styled": "2.5.0", + "chameleon-transformers": "1.0.0", + "channel": "1.0.0", + "chartjs": "0.2.0", + "chartjs-halogen": "0.2.0", + "checked-exceptions": "3.1.1", + "choku": "1.0.2", + "classless": "0.1.1", + "classless-arbitrary": "0.1.1", + "classless-decode-json": "0.1.1", + "classless-encode-json": "0.1.3", + "classnames": "2.0.0", + "codec": "6.1.0", + "codec-argonaut": "10.0.0", + "codec-json": "2.0.0", + "colors": "7.0.1", + "compile-fail": "0.4.0", + "concur-core": "0.5.0", + "concur-react": "0.5.0", + "concurrent-queues": "3.0.0", + "console": "6.1.0", + "const": "6.0.0", + "contravariant": "6.0.0", + "control": "6.0.0", + "convertable-options": "1.0.0", + "coroutines": "7.0.0", + "css": "6.0.0", + "css-class-name-extractor": "0.0.4", + "css-frameworks": "1.1.0", + "csv-stream": "2.3.0", + "currency": "0.1.0", + "data-mvc": "0.0.2", + "datetime": "6.1.0", + "datetime-parsing": "0.2.0", + "debounce": "0.1.2", + "debug": "6.0.2", + "decimals": "7.1.0", + "default-values": "1.0.1", + "deku": "0.9.24", + "deno": "0.0.5", + "dissect": "1.0.0", + "distributive": "6.0.0", + "dodo-printer": "2.2.3", + "dom-filereader": "7.0.0", + "dom-indexed": "13.0.0", + "dom-simple": "0.4.0", + "dotenv": "4.0.3", + "droplet": "0.6.0", + "dts": "1.0.0", + "dual-numbers": "1.0.3", + "dynamic-buffer": "3.0.1", + "echarts-simple": "0.0.1", + "effect": "4.0.0", + "either": "6.1.0", + "elmish": "0.15.0", + "elmish-enzyme": "0.1.1", + "elmish-hooks": "0.12.0", + "elmish-html": "0.14.0", + "elmish-testing-library": "0.5.0", + "email-validate": "7.0.0", + "encoding": "0.0.10", + "enums": "6.0.1", + "env-names": "0.4.0", + "environment": "2.0.0", + "error": "2.0.0", + "eta-conversion": "0.3.2", + "exceptions": "6.1.0", + "exists": "6.0.0", + "exitcodes": "4.0.0", + "expect-inferred": "3.0.0", + "express": "0.9.1", + "ezfetch": "1.1.1", + "fahrtwind": "2.0.0", + "faker-ffi": "0.1.1", + "fakerjs": "0.0.1", + "fallback": "0.1.0", + "fast-vect": "1.2.0", + "fetch": "4.1.0", + "fetch-argonaut": "1.0.1", + "fetch-core": "5.1.0", + "fetch-yoga-json": "1.1.0", + "ffi-simple": "0.5.1", + "fft": "0.3.0", + "fft-js": "0.1.0", + "filterable": "5.0.0", + "fix-functor": "0.1.0", + "fixed-points": "7.0.0", + "fixed-precision": "5.0.0", + "flame": "1.6.0", + "float32": "2.0.0", + "fmt": "0.2.1", + "foldable-traversable": "6.0.0", + "foldable-traversable-extra": "0.0.6", + "foreign": "7.0.0", + "foreign-object": "4.1.0", + "foreign-readwrite": "3.4.0", + "forgetmenot": "0.1.0", + "fork": "6.0.0", + "form-urlencoded": "7.0.0", + "formatters": "7.0.0", + "framer-motion": "3.1.0", + "free": "7.1.0", + "freeap": "7.0.0", + "freer-free": "0.0.1", + "freet": "7.0.0", + "functions": "6.0.0", + "functor1": "3.0.0", + "functors": "5.0.0", + "fuzzy": "0.4.0", + "gen": "4.0.0", + "generate-values": "1.0.1", + "generic-router": "0.0.1", + "geojson": "0.0.5", + "geometria": "2.2.0", + "gesso": "1.0.1", + "gojs": "0.1.1", + "golden-test": "0.1.0", + "golem-fetch": "0.1.0", + "grain": "3.0.0", + "grain-router": "3.0.0", + "grain-virtualized": "3.0.0", + "graphql-client": "10.1.1", + "graphs": "8.1.0", + "group": "4.1.1", + "halogen": "7.0.0", + "halogen-bootstrap5": "5.3.2", + "halogen-canvas": "1.0.0", + "halogen-css": "10.0.0", + "halogen-declarative-canvas": "0.0.8", + "halogen-echarts-simple": "0.0.4", + "halogen-formless": "4.0.3", + "halogen-helix": "1.1.0", + "halogen-hooks": "0.6.3", + "halogen-hooks-extra": "0.9.0", + "halogen-infinite-scroll": "1.1.0", + "halogen-store": "0.5.4", + "halogen-storybook": "2.0.0", + "halogen-subscriptions": "2.0.0", + "halogen-svg-elems": "8.0.0", + "halogen-typewriter": "1.0.4", + "halogen-use-trigger-hooks": "1.0.0", + "halogen-vdom": "8.0.0", + "halogen-vdom-string-renderer": "0.5.0", + "halogen-widgets": "0.3.1", + "halogen-xterm": "2.0.0", + "harmonia": "0.2.0", + "heckin": "2.0.1", + "heterogeneous": "0.7.0", + "homogeneous": "0.4.0", + "http-methods": "6.1.0", + "httpurple": "4.0.0", + "huffman": "0.4.0", + "humdrum": "0.0.1", + "hylograph-canvas": "0.1.0", + "hylograph-components": "0.1.0", + "hylograph-d3-kernel": "0.1.0", + "hylograph-graph": "0.3.0", + "hylograph-layout": "0.3.0", + "hylograph-music": "0.4.0", + "hylograph-optics": "0.1.0", + "hylograph-selection": "0.5.3", + "hylograph-simulation": "0.6.0", + "hylograph-simulation-core": "0.1.0", + "hylograph-simulation-halogen": "0.5.1", + "hylograph-transitions": "0.1.0", + "hylograph-wasm-kernel": "0.1.0", + "hyrule": "2.3.9", + "i18next": "0.1.0", + "idb": "0.2.0", + "identity": "6.0.0", + "identy": "4.0.1", + "indexed-db": "1.0.0", + "indexed-monad": "3.0.0", + "ink": "0.0.1", + "int64": "3.0.0", + "integers": "6.0.0", + "interpolate": "5.0.2", + "intersection-observer": "1.0.1", + "invariant": "6.0.0", + "jarilo": "1.0.1", + "jelly": "0.10.0", + "jelly-router": "0.3.0", + "jelly-signal": "0.4.0", + "jest": "1.0.0", + "js-abort-controller": "1.0.0", + "js-bigints": "2.2.1", + "js-date": "8.0.0", + "js-fetch": "0.2.1", + "js-fileio": "3.0.0", + "js-intl": "1.3.0", + "js-iterators": "0.1.1", + "js-maps": "0.1.2", + "js-promise": "1.0.0", + "js-promise-aff": "1.0.0", + "js-temporal": "3.0.0", + "js-timers": "6.1.0", + "js-uri": "3.1.0", + "jsdom": "1.0.0", + "jsinc": "0.5.1", + "json": "1.1.0", + "json-codecs": "5.0.0", + "justifill": "0.5.0", + "jwt": "0.0.9", + "labeled-data": "0.2.0", + "language-cst-parser": "0.14.1", + "lazy": "6.0.0", + "lazy-joe": "1.0.0", + "lcg": "4.0.0", + "leibniz": "5.0.0", + "leveldb": "1.0.1", + "liminal": "1.0.1", + "linalg": "6.0.0", + "linear": "0.1.0", + "lists": "7.0.0", + "literals": "1.0.2", + "logging": "3.0.0", + "logging-journald": "0.4.0", + "lumi-components": "18.0.0", + "machines": "7.0.0", + "maps-eager": "0.5.0", + "marionette": "1.0.0", + "marionette-react-basic-hooks": "0.1.1", + "markdown-it-js": "0.1.0", + "marked": "0.1.0", + "matrices": "5.0.1", + "matryoshka": "1.0.0", + "maybe": "6.0.0", + "media-types": "6.0.0", + "meowclient": "1.0.0", + "midi": "4.0.0", + "milkdown": "0.1.0", + "milkis": "9.0.0", + "mimetype": "0.0.1", + "minibench": "4.0.1", + "mmorph": "7.0.0", + "monad-control": "5.0.0", + "monad-logger": "1.3.1", + "monad-loops": "0.5.0", + "monad-unlift": "1.0.1", + "monoid-extras": "0.0.1", + "monoidal": "0.16.0", + "mote": "3.0.0", + "motion": "4.0.0", + "motsunabe": "2.0.0", + "mvc": "0.0.1", + "mycroft": "0.1.1", + "mycroft-z3-wasm": "0.1.0", + "mysql": "6.0.1", + "n3": "0.1.0", + "nano-id": "1.1.0", + "nanoid": "0.1.0", + "naturals": "3.0.0", + "nested-functor": "0.2.1", + "newtype": "5.0.0", + "next-purs-rsc": "0.1.0", + "nextjs": "0.1.1", + "nextui": "0.2.0", + "node-buffer": "9.0.0", + "node-child-process": "11.1.0", + "node-event-emitter": "3.0.0", + "node-execa": "5.0.0", + "node-fs": "9.2.0", + "node-glob-basic": "2.0.0", + "node-http": "9.1.0", + "node-http2": "1.1.1", + "node-human-signals": "1.0.0", + "node-net": "5.1.0", + "node-os": "5.1.0", + "node-path": "5.0.1", + "node-process": "11.2.0", + "node-readline": "8.1.1", + "node-sqlite": "0.2.0", + "node-sqlite3": "8.0.0", + "node-stream-pipes": "2.1.6", + "node-streams": "9.0.1", + "node-tls": "0.3.1", + "node-url": "7.0.1", + "node-workerbees": "0.3.1", + "node-zlib": "0.4.0", + "nonempty": "7.0.0", + "now": "6.0.0", + "npm-package-json": "2.0.0", + "nqueens": "0.1.4", + "nullable": "6.0.0", + "numberfield": "0.2.2", + "numbers": "9.0.1", + "oak": "3.1.1", + "oak-debug": "1.2.2", + "object-maps": "0.3.0", + "ocarina": "1.5.4", + "oooooooooorrrrrrrmm-lib": "0.0.1", + "open-colors-scales-and-schemes": "1.0.0", + "open-drawing": "6.0.4", + "open-folds": "6.4.0", + "open-foreign-generic": "11.0.3", + "open-memoize": "6.2.0", + "open-mkdirp-aff": "1.2.0", + "open-pairing": "6.2.0", + "open-smolder": "12.0.2", + "options": "7.0.0", + "optparse": "5.0.1", + "ordered-collections": "3.2.0", + "ordered-set": "0.5.2", + "orders": "6.0.0", + "org-doc": "0.1.0", + "owoify": "1.2.0", + "pairs": "9.0.1", + "parallel": "7.0.0", + "parsing": "11.0.0", + "parsing-dataview": "3.2.4", + "partial": "4.0.0", + "pathy": "9.0.0", + "pha": "0.13.0", + "phaser": "0.7.0", + "phylio": "1.1.2", + "pipes": "8.0.0", + "pirates-charm": "0.0.1", + "play": "1.0.0", + "pmock": "0.9.0", + "point-free": "1.0.0", + "pointed-list": "0.5.1", + "polymorphic-vectors": "4.0.0", + "posix-types": "6.0.0", + "postgresql": "2.0.21", + "precise": "6.0.0", + "precise-datetime": "7.0.0", + "prelude": "6.0.2", + "prettier-printer": "3.0.0", + "printf": "0.1.0", + "priority-queue": "0.1.2", + "profunctor": "6.0.1", + "profunctor-lenses": "8.0.0", + "prospero": "0.1.0", + "protobuf": "4.4.0", + "ps-spa": "0.7.1", + "psa-utils": "8.0.0", + "psci-support": "6.0.0", + "punycode": "1.0.0", + "pursfmt": "0.17.0", + "qualified-do": "2.2.0", + "quantities": "12.2.0", + "quickcheck": "8.0.1", + "quickcheck-combinators": "0.1.3", + "quickcheck-laws": "7.0.0", + "quickcheck-utf8": "0.0.0", + "random": "6.0.0", + "rationals": "6.0.0", + "rdf": "0.1.0", + "react": "11.0.0", + "react-aria": "0.2.0", + "react-basic": "17.0.0", + "react-basic-classic": "3.0.0", + "react-basic-dnd": "10.1.0", + "react-basic-dom": "7.0.0", + "react-basic-dom-beta": "0.1.1", + "react-basic-emotion": "7.1.0", + "react-basic-hooks": "9.1.1", + "react-basic-storybook": "2.0.0", + "react-dnd-kit": "0.6.0", + "react-dom": "8.0.0", + "react-halo": "3.0.0", + "react-icons": "1.1.6", + "react-markdown": "0.1.0", + "react-testing-library": "4.0.1", + "react-virtuoso": "1.0.0", + "reactix": "0.6.1", + "read": "1.0.1", + "recharts": "1.1.0", + "record": "4.0.0", + "record-extra": "5.0.1", + "record-extra-srghma": "0.2.8", + "record-ptional-fields": "0.1.2", + "record-studio": "1.0.4", + "ref-lifted": "0.0.2", + "refs": "6.0.0", + "remotedata": "5.0.1", + "repr": "0.5.0", + "resize-arrays": "0.0.1", + "resize-observer": "1.0.0", + "resource": "2.0.1", + "resourcet": "1.0.0", + "result": "1.0.3", + "return": "0.2.0", + "ring-modules": "5.0.1", + "rito": "0.3.4", + "roman": "0.4.0", + "rough-notation": "1.0.2", + "routing": "11.0.0", + "routing-duplex": "0.7.0", + "run": "5.0.0", + "safe-coerce": "2.0.0", + "safely": "4.0.1", + "school-of-music": "1.3.0", + "selection-foldable": "0.2.0", + "selective-functors": "1.0.1", + "semirings": "7.0.0", + "shuffle": "2.0.0", + "sigil": "0.3.0", + "sigil-hats": "0.3.0", + "signal": "13.0.0", + "simple-emitter": "3.0.1", + "simple-i18n": "2.0.1", + "simple-json": "9.0.0", + "simple-json-generics": "0.2.1", + "simple-ulid": "3.0.0", + "sized-matrices": "1.0.0", + "sized-vectors": "5.0.2", + "slug": "3.2.0", + "small-ffi": "4.0.1", + "soundfonts": "4.1.0", + "sparse-matrices": "2.0.1", + "sparse-polynomials": "3.0.1", + "spec": "8.1.2", + "spec-discovery": "8.4.1", + "spec-mocha": "5.1.1", + "spec-node": "0.0.3", + "spec-quickcheck": "5.0.2", + "spec-reporter-xunit": "0.7.1", + "splitmix": "2.1.0", + "ssrs": "1.0.0", + "st": "6.2.0", + "statistics": "0.3.2", + "strictlypositiveint": "1.0.1", + "string-parsers": "8.0.0", + "strings": "6.0.1", + "strings-extra": "4.0.0", + "stringutils": "0.0.13", + "structured-logging": "1.0.0", + "substitute": "0.2.3", + "supabase": "0.1.0", + "supply": "0.2.0", + "svg-parser": "3.0.0", + "systemd-journald": "0.3.0", + "tagged": "4.0.2", + "tailrec": "6.1.0", + "tanstack-query": "3.0.0", + "tecton": "0.2.1", + "tecton-halogen": "0.2.0", + "temporal": "0.2.0", + "test-unit": "17.0.0", + "text-formatting": "0.1.0", + "thermite": "6.3.1", + "thermite-dom": "0.3.1", + "these": "6.0.0", + "threading": "0.0.3", + "tidy": "0.11.1", + "tidy-codegen": "4.0.1", + "tldr": "0.0.0", + "toestand": "0.9.0", + "transformation-matrix": "1.0.1", + "transformers": "6.1.0", + "transit": "1.0.0", + "tree-rose": "4.0.2", + "trivial-unfold": "0.5.0", + "ts-bridge": "4.0.0", + "tuples": "7.0.0", + "two-or-more": "1.0.0", + "type-equality": "4.0.1", + "typedenv": "2.0.1", + "typelevel": "6.0.0", + "typelevel-lists": "2.1.0", + "typelevel-peano": "1.0.1", + "typelevel-prelude": "7.0.0", + "typelevel-regex": "0.0.3", + "typelevel-rows": "0.1.0", + "typisch": "0.4.0", + "uint": "7.0.0", + "ulid": "3.0.1", + "uncurried-transformers": "1.1.0", + "undefined": "2.0.0", + "undefined-is-not-a-problem": "1.1.0", + "unfoldable": "6.0.0", + "unicode": "6.0.0", + "unique": "0.6.1", + "unlift": "1.0.1", + "unordered-collections": "3.1.0", + "unsafe-coerce": "6.0.0", + "unsafe-reference": "5.0.0", + "untagged-to-tagged": "0.1.4", + "untagged-union": "1.0.0", + "uri": "9.0.0", + "url-immutable": "1.0.0", + "url-regex-safe": "0.1.1", + "uuid": "9.0.0", + "uuidv4": "1.0.0", + "validation": "6.0.0", + "variant": "8.0.0", + "variant-encodings": "2.0.0", + "variant-gen": "1.0.0", + "vectorfield": "1.0.1", + "vectors": "2.1.0", + "versions": "7.0.0", + "visx": "0.0.2", + "vitest": "2.0.1", + "wasm-base": "0.2.0", + "web-chain": "1.0.1", + "web-clipboard": "6.0.0", + "web-cssom": "2.0.0", + "web-cssom-view": "0.1.0", + "web-dom": "6.0.0", + "web-dom-parser": "8.0.0", + "web-dom-xpath": "3.0.0", + "web-encoding": "3.0.0", + "web-events": "4.0.0", + "web-fetch": "4.0.1", + "web-file": "4.0.0", + "web-geometry": "0.1.0", + "web-html": "4.1.1", + "web-pointerevents": "2.0.0", + "web-proletarian": "1.0.0", + "web-promise": "3.2.0", + "web-resize-observer": "2.1.0", + "web-router": "1.0.0", + "web-socket": "4.0.0", + "web-storage": "5.0.0", + "web-streams": "4.0.0", + "web-touchevents": "4.0.0", + "web-uievents": "5.0.0", + "web-url": "2.0.0", + "web-workers": "2.0.0", + "web-xhr": "5.0.1", + "webb-aff-list": "0.0.2", + "webb-array": "0.0.2", + "webb-channel": "0.0.3", + "webb-commandline": "0.0.3", + "webb-directory": "0.0.7", + "webb-file": "0.0.1", + "webb-map": "0.0.1", + "webb-monad": "0.0.2", + "webb-mutex": "0.0.7", + "webb-parsing": "0.0.5", + "webb-random": "0.0.1", + "webb-refer": "0.0.3", + "webb-set": "0.0.1", + "webb-slot": "0.0.1", + "webb-stateful": "0.0.3", + "webb-string": "0.0.1", + "webb-test": "0.0.1", + "webb-thread": "0.0.2", + "webb-writer": "0.0.3", + "webextension-polyfill": "0.1.0", + "webgpu": "0.0.1", + "which": "2.0.0", + "whine-core": "0.0.34", + "xterm": "1.0.0", + "yaml-next": "3.1.1", + "yoga-acp-om": "0.1.1", + "yoga-better-auth": "0.2.2", + "yoga-bun-yoga": "0.2.1", + "yoga-config": "0.2.1", + "yoga-country": "3.0.0", + "yoga-docker-compose": "0.1.1", + "yoga-dynamodb": "0.1.1", + "yoga-elasticsearch": "0.1.1", + "yoga-fastify": "0.5.4", + "yoga-fastify-om": "0.4.7", + "yoga-fetch": "1.0.1", + "yoga-fetch-om": "0.8.0", + "yoga-format": "1.0.0", + "yoga-heroui": "2.0.3", + "yoga-http-api": "0.3.4", + "yoga-jaeger": "0.1.1", + "yoga-json": "5.2.1", + "yoga-language": "1.0.0", + "yoga-next-fastify": "0.2.0", + "yoga-om": "2.2.1", + "yoga-om-layer": "2.1.0", + "yoga-om-strom": "0.4.3", + "yoga-om-workerbees": "0.1.2", + "yoga-opentelemetry": "0.2.0", + "yoga-options": "0.1.1", + "yoga-pino": "0.1.1", + "yoga-postgres": "6.0.0", + "yoga-react": "1.0.2", + "yoga-react-dom": "2.0.1", + "yoga-react-native": "0.2.1", + "yoga-react-om": "0.2.0", + "yoga-redis": "0.1.1", + "yoga-scylladb": "0.1.1", + "yoga-shadcn": "1.0.5", + "yoga-sql-types": "0.2.0", + "yoga-sqlite": "0.3.4", + "yoga-sqlite-om": "0.3.0", + "yoga-sse": "0.1.1", + "yoga-subtlecrypto": "0.1.0", + "yoga-test-docker": "0.1.2", + "yoga-timezone": "1.0.0", + "yoga-tree": "1.0.0", + "yoga-tree-svg": "0.1.0", + "yoga-tree-utils": "1.0.0", + "z3": "0.0.2", + "zipperarray": "2.0.0" + } + }, + "extra_packages": {} + }, + "packages": { + "aff": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=", + "dependencies": [ + "control", + "datetime", + "effect", + "either", + "exceptions", + "functions", + "newtype", + "parallel", + "partial", + "prelude", + "st", + "tailrec", + "transformers", + "unsafe-coerce" + ] + }, + "aff-promise": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-Jgp3y+NWuuAmwz2V3LlWoX+AvxqfVglY77N5zTw8xnI=", + "dependencies": [ + "aff", + "control", + "effect", + "either", + "exceptions", + "foreign", + "prelude", + "transformers" + ] + }, + "ansi": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-tOUaXJFzPSfBVAqLI8hc+Tz2M1rg16cXqyHaK/3SmFg=", + "dependencies": [ + "foldable-traversable", + "lists", + "prelude" + ] + }, + "argonaut-codecs": { + "type": "registry", + "version": "9.1.0", + "integrity": "sha256-K910SBrmYallESsm8pxJYThs7lig6hxX3M4PW33Cgew=", + "dependencies": [ + "argonaut-core", + "arrays", + "bifunctors", + "either", + "foldable-traversable", + "foreign-object", + "identity", + "integers", + "lists", + "maybe", + "nonempty", + "ordered-collections", + "prelude", + "record", + "strings", + "tuples" + ] + }, + "argonaut-core": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-gYZihtBIKthVuXXoiPphMookSf8M5aHyFYkcIWqlxTM=", + "dependencies": [ + "arrays", + "control", + "either", + "foreign-object", + "functions", + "gen", + "maybe", + "nonempty", + "prelude", + "strings", + "tailrec" + ] + }, + "arraybuffer-types": { + "type": "registry", + "version": "3.0.2", + "integrity": "sha256-p05cJnSkyeoB7VHzMyc2Eb1RwUaB7Nl0sQSqEGNEUD8=", + "dependencies": [] + }, + "arrays": { + "type": "registry", + "version": "7.3.0", + "integrity": "sha256-GD4z7LCi9wzi7FCQqbWhvs8paoUK9NO+HwgXZ+KP8Rk=", + "dependencies": [ + "bifunctors", + "control", + "foldable-traversable", + "functions", + "maybe", + "nonempty", + "partial", + "prelude", + "safe-coerce", + "st", + "tailrec", + "tuples", + "unfoldable", + "unsafe-coerce" + ] + }, + "avar": { + "type": "registry", + "version": "5.0.1", + "integrity": "sha256-8V4SxF4TIWZ9Ik1P4lPzIRUAZeFBe8w5WA2VcCEKsIk=", + "dependencies": [ + "aff", + "effect", + "either", + "exceptions", + "functions", + "maybe", + "prelude" + ] + }, + "bifunctors": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-CvZRb8uI75eKIqiYntR+k05Wk6tKlMS5w8sCA2AFna0=", + "dependencies": [ + "const", + "either", + "newtype", + "prelude", + "tuples" + ] + }, + "catenable-lists": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-7+i/MHBLIRh604iCNT4ENWRWE/wuHIotER4kpdX9Ve0=", + "dependencies": [ + "control", + "foldable-traversable", + "lists", + "maybe", + "prelude", + "tuples", + "unfoldable" + ] + }, + "console": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-3T5PPz1mATZmV0C4GzkCkpLlCa5xG7u8ZNgnLrrrI1Q=", + "dependencies": [ + "effect", + "prelude" + ] + }, + "const": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-S+m0lQsxnji0z1XRUmkn+3RFuWJ5CXWuIr1u3i8Bfoc=", + "dependencies": [ + "invariant", + "newtype", + "prelude" + ] + }, + "contravariant": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-nuckEWqL80NaPmR/l9AUyrfycNh18XcvlZV9PAamwqc=", + "dependencies": [ + "const", + "either", + "newtype", + "prelude", + "tuples" + ] + }, + "control": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-0vxyYh5FL+ilxgLo954w1LqCUC0RSIivNNlb9CaNUQg=", + "dependencies": [ + "newtype", + "prelude" + ] + }, + "datetime": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-tbv6eDXy6QOD7YSknxYSbsDF32OC2JrNbSBpzMs7Doc=", + "dependencies": [ + "bifunctors", + "control", + "either", + "enums", + "foldable-traversable", + "functions", + "gen", + "integers", + "lists", + "maybe", + "newtype", + "numbers", + "ordered-collections", + "partial", + "prelude", + "tuples" + ] + }, + "distributive": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-netyJK4B32PDYHD0tF2AenKgo3urzbI770ycM0pArAo=", + "dependencies": [ + "identity", + "newtype", + "prelude", + "tuples", + "type-equality" + ] + }, + "effect": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-DdqU3bncRTjgMVPPRlAuGHVeajSPEcRaRhNuio7if6s=", + "dependencies": [ + "prelude" + ] + }, + "either": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-tBHx3PgtH4GZOApZCDkcM7szRTYJinrUrVGWazQCUUg=", + "dependencies": [ + "control", + "invariant", + "maybe", + "prelude" + ] + }, + "enums": { + "type": "registry", + "version": "6.0.1", + "integrity": "sha256-sdZOmLX5+5pASGpvVyT0vSD5BBYQjZiayjV5XiPutso=", + "dependencies": [ + "control", + "either", + "gen", + "maybe", + "newtype", + "nonempty", + "partial", + "prelude", + "tuples", + "unfoldable" + ] + }, + "exceptions": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=", + "dependencies": [ + "effect", + "either", + "maybe", + "prelude" + ] + }, + "exists": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-vtLbrWNaI+pzx/2fw2AQnCovoLtcosClIhjO26QKkh8=", + "dependencies": [ + "unsafe-coerce" + ] + }, + "exitcodes": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-xBl4aFSEQ05Owp1ZvO6OxxvLK3LVzU35RSbj79SsePQ=", + "dependencies": [ + "enums", + "maybe", + "prelude" + ] + }, + "foldable-traversable": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-NGGWyCio/xjce6fPP7y3wwg7CheqllID6aJudDxbWhA=", + "dependencies": [ + "bifunctors", + "const", + "control", + "either", + "functors", + "identity", + "maybe", + "newtype", + "orders", + "prelude", + "tuples" + ] + }, + "foreign": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-jiDRMVQfovGsbbA/FzbzYf6iWk9i2b5gNrIjz+gFfsI=", + "dependencies": [ + "either", + "functions", + "integers", + "lists", + "maybe", + "prelude", + "strings", + "transformers", + "unsafe-coerce" + ] + }, + "foreign-object": { + "type": "registry", + "version": "4.1.0", + "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=", + "dependencies": [ + "arrays", + "foldable-traversable", + "functions", + "gen", + "lists", + "maybe", + "prelude", + "st", + "tailrec", + "tuples", + "typelevel-prelude", + "unfoldable", + "unsafe-coerce" + ] + }, + "fork": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=", + "dependencies": [ + "aff", + "prelude", + "transformers" + ] + }, + "free": { + "type": "registry", + "version": "7.1.0", + "integrity": "sha256-tUBInfUpRQEw4u94GWcYW7EYdmSdDHSRn88a+C0eltU=", + "dependencies": [ + "catenable-lists", + "control", + "distributive", + "either", + "exists", + "foldable-traversable", + "invariant", + "lazy", + "maybe", + "prelude", + "tailrec", + "transformers", + "tuples", + "unsafe-coerce" + ] + }, + "functions": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-0jgtOr+SFO4UScKc40oA/9Vdcf2rLn3h3vZlY0KfqVo=", + "dependencies": [ + "prelude" + ] + }, + "functors": { + "type": "registry", + "version": "5.0.0", + "integrity": "sha256-W1o/4wcp6S3chodYL8uixlXK2mfr1L+SzM1R+whqUco=", + "dependencies": [ + "bifunctors", + "const", + "contravariant", + "control", + "distributive", + "either", + "invariant", + "maybe", + "newtype", + "prelude", + "profunctor", + "tuples", + "unsafe-coerce" + ] + }, + "gen": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-kyOateeOtQa07vfTMYeSuCKdelgKE1R65fwKkZ10wsY=", + "dependencies": [ + "either", + "foldable-traversable", + "identity", + "maybe", + "newtype", + "nonempty", + "prelude", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "halogen-subscriptions": { + "type": "registry", + "version": "2.0.0", + "integrity": "sha256-1eBtVZENgGtKuOY9H0iuYD3dO1CSqmOIyhYe4OhypOU=", + "dependencies": [ + "arrays", + "contravariant", + "control", + "effect", + "foldable-traversable", + "maybe", + "prelude", + "refs", + "safe-coerce", + "unsafe-reference" + ] + }, + "identity": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-RY/iXPpxpqvKHYfJeFVNI1J06kohB9rbtWYuha8t0LE=", + "dependencies": [ + "control", + "invariant", + "newtype", + "prelude" + ] + }, + "indexed-monad": { + "type": "registry", + "version": "3.0.0", + "integrity": "sha256-84PslUkPKyrFUGeq981auE+zODsQO/g8rnqwgcN0zQM=", + "dependencies": [ + "newtype", + "prelude" + ] + }, + "integers": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-Y8yozC1uHRLcruobEyv5/bMKzE1BoKJ/olHv6o+bgm8=", + "dependencies": [ + "maybe", + "numbers", + "prelude" + ] + }, + "invariant": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-HyoT1I5qIHRq8RSZji6zZDvqnpVjLw748utrKwZtqsw=", + "dependencies": [ + "control", + "prelude" + ] + }, + "js-date": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-DNQrp4xYc8o80jtxo0aOkUeXGi6h0Xidkk2ZQ8Y4l5Y=", + "dependencies": [ + "datetime", + "effect", + "enums", + "foreign", + "functions", + "integers", + "maybe", + "prelude" + ] + }, + "lazy": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-MHRC+1Pc+AjortBp4b/e1e6KpEpsDwaJBNBB7TOL+1I=", + "dependencies": [ + "control", + "foldable-traversable", + "invariant", + "prelude" + ] + }, + "lists": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-/mGd7wLoU+wsTcqii7SSUByxsvwvjnu2PjwAD47yYb4=", + "dependencies": [ + "bifunctors", + "control", + "foldable-traversable", + "lazy", + "maybe", + "newtype", + "nonempty", + "partial", + "prelude", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "maybe": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-8X6XEgtZ5lqfkEuc1hWOofnL6mwhk8gSJTxwvbR/SbY=", + "dependencies": [ + "control", + "invariant", + "newtype", + "prelude" + ] + }, + "media-types": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-3Wyq+vfqJFTmy1k6DGVnXStxtZxgK631wusDPzlFVxg=", + "dependencies": [ + "newtype", + "prelude" + ] + }, + "mmorph": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=", + "dependencies": [ + "bifunctors", + "either", + "free", + "functors", + "identity", + "maybe", + "newtype", + "prelude", + "transformers", + "tuples" + ] + }, + "newtype": { + "type": "registry", + "version": "5.0.0", + "integrity": "sha256-dt6w0cty4OS+Pjt9fsh3iU6ottqSU83mdAZkaEEvo6c=", + "dependencies": [ + "prelude", + "safe-coerce" + ] + }, + "node-buffer": { + "type": "registry", + "version": "9.0.0", + "integrity": "sha256-RdQjilmEHqsT4fwpCr8Mavw8KTafYrS3ZM6zeAAVAl8=", + "dependencies": [ + "arraybuffer-types", + "effect", + "functions", + "maybe", + "nullable", + "partial", + "prelude", + "st", + "unsafe-coerce" + ] + }, + "node-event-emitter": { + "type": "registry", + "version": "3.0.0", + "integrity": "sha256-wodx71NxJBPXOaawFn01V1ByYB2vT+I3RuV2giOVKMg=", + "dependencies": [ + "effect", + "either", + "functions", + "maybe", + "nullable", + "prelude", + "unsafe-coerce" + ] + }, + "node-fs": { + "type": "registry", + "version": "9.2.0", + "integrity": "sha256-fxioTSm9y47WuYs9F/7nxCh/RVDCl3Kr3Hg779J4SJA=", + "dependencies": [ + "aff", + "datetime", + "effect", + "either", + "enums", + "exceptions", + "functions", + "integers", + "js-date", + "maybe", + "node-buffer", + "node-path", + "node-streams", + "nullable", + "partial", + "prelude", + "strings" + ] + }, + "node-path": { + "type": "registry", + "version": "5.0.1", + "integrity": "sha256-j7n/SmpVz0FOMJl4XZop3ofJ+MeIPkMNqUEUKHEL6Us=", + "dependencies": [ + "effect" + ] + }, + "node-process": { + "type": "registry", + "version": "11.2.0", + "integrity": "sha256-onzKeNmaeYfN3HSDPqSFTrrP2Yl9tnVsfKKhnf2plxM=", + "dependencies": [ + "effect", + "exceptions", + "foreign", + "foreign-object", + "maybe", + "node-event-emitter", + "node-streams", + "nullable", + "posix-types", + "prelude", + "strings" + ] + }, + "node-streams": { + "type": "registry", + "version": "9.0.1", + "integrity": "sha256-yuVGm/R/tBr4Nphi+a1a984Z0fkmN9Q5wWtSmbmJTpA=", + "dependencies": [ + "aff", + "arrays", + "effect", + "either", + "exceptions", + "maybe", + "node-buffer", + "node-event-emitter", + "nullable", + "prelude", + "refs", + "st", + "tailrec", + "unsafe-coerce" + ] + }, + "nonempty": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-Ctkq8/+KiGqCE53Y/LvibA/coy7ev9HtRZU+GYS7yfQ=", + "dependencies": [ + "control", + "foldable-traversable", + "maybe", + "prelude", + "tuples", + "unfoldable" + ] + }, + "now": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=", + "dependencies": [ + "datetime", + "effect", + "prelude" + ] + }, + "nullable": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=", + "dependencies": [ + "functions", + "maybe", + "prelude" + ] + }, + "numbers": { + "type": "registry", + "version": "9.0.1", + "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=", + "dependencies": [ + "functions", + "maybe", + "prelude" + ] + }, + "open-memoize": { + "type": "registry", + "version": "6.2.0", + "integrity": "sha256-yJLuVYX8FocHIJ+6q7efH/Pj4S+e1Y57DZ5Dn7qf4ww=", + "dependencies": [ + "either", + "integers", + "lazy", + "lists", + "maybe", + "partial", + "prelude", + "strings", + "tuples" + ] + }, + "optparse": { + "type": "registry", + "version": "5.0.1", + "integrity": "sha256-2btDjHPRR0uujUQJuZAN9AzvVoXe7yG6IwnY5evERhQ=", + "dependencies": [ + "arrays", + "bifunctors", + "control", + "effect", + "either", + "enums", + "exists", + "exitcodes", + "foldable-traversable", + "free", + "integers", + "lazy", + "lists", + "maybe", + "newtype", + "node-buffer", + "node-process", + "node-streams", + "nonempty", + "numbers", + "open-memoize", + "partial", + "prelude", + "strings", + "tailrec", + "transformers", + "tuples" + ] + }, + "ordered-collections": { + "type": "registry", + "version": "3.2.0", + "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=", + "dependencies": [ + "arrays", + "control", + "foldable-traversable", + "functions", + "gen", + "lists", + "maybe", + "newtype", + "partial", + "prelude", + "safe-coerce", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "orders": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-nSMxZK7wxyDULkjZqvMyB71mVo6b0AyLLHosDVbv1XM=", + "dependencies": [ + "newtype", + "prelude" + ] + }, + "parallel": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=", + "dependencies": [ + "control", + "effect", + "either", + "foldable-traversable", + "functors", + "maybe", + "newtype", + "prelude", + "profunctor", + "refs", + "transformers" + ] + }, + "partial": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-mdrlJBAgFMh79hNRW39uv7c+BwnnaOrAMmdh7+VhEhs=", + "dependencies": [] + }, + "pipes": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=", + "dependencies": [ + "aff", + "control", + "effect", + "either", + "foldable-traversable", + "identity", + "lists", + "maybe", + "mmorph", + "newtype", + "prelude", + "tailrec", + "transformers", + "tuples" + ] + }, + "posix-types": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-Je5jG439F/SFO/IvVevZnWVxDIonrehlKuJkefP2lHE=", + "dependencies": [ + "maybe", + "newtype", + "prelude" + ] + }, + "prelude": { + "type": "registry", + "version": "6.0.2", + "integrity": "sha256-IqykrDRquGUD/LRSad7y75hZv3bUsGQ4knc8tdez8Qw=", + "dependencies": [] + }, + "profunctor": { + "type": "registry", + "version": "6.0.1", + "integrity": "sha256-Ow0mJC+PIIRq/a3mzCWY5vxuTgR/e+Ee68+mWeSRnWY=", + "dependencies": [ + "control", + "distributive", + "either", + "exists", + "invariant", + "newtype", + "prelude", + "tuples" + ] + }, + "react-basic": { + "type": "registry", + "version": "17.0.0", + "integrity": "sha256-RZA6jqfbrFHLXYD1ARlnfHp2YkT9IjUTkUwsk7Rqn8s=", + "dependencies": [ + "effect", + "prelude", + "record" + ] + }, + "react-basic-dom": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-q5WQnD1cEVe/hzWDziZfPM9DclgCUBowRgbBg1xReSA=", + "dependencies": [ + "arrays", + "effect", + "foldable-traversable", + "foreign-object", + "maybe", + "nullable", + "prelude", + "react-basic", + "record", + "unsafe-coerce", + "web-dom", + "web-events", + "web-file", + "web-html" + ] + }, + "react-basic-hooks": { + "type": "registry", + "version": "9.1.1", + "integrity": "sha256-wUZMNNivUj6yJDk3rbgeEgljfv2gq1bPHKHEJbkzyNQ=", + "dependencies": [ + "aff", + "aff-promise", + "bifunctors", + "console", + "control", + "datetime", + "effect", + "either", + "exceptions", + "foldable-traversable", + "foreign-object", + "functions", + "indexed-monad", + "integers", + "maybe", + "newtype", + "now", + "nullable", + "ordered-collections", + "prelude", + "react-basic", + "refs", + "tuples", + "type-equality", + "unsafe-coerce", + "unsafe-reference", + "web-html" + ] + }, + "record": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-WQ8fqp4X1wTA70J4qUcPW4Z7DCwx6e5KJnT/EuTdWSc=", + "dependencies": [ + "functions", + "prelude", + "unsafe-coerce" + ] + }, + "refs": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-KKD5G9dR2SauuDGULUJsxjqDOEi1Jqm+2Sq5U2mY8YU=", + "dependencies": [ + "effect", + "prelude" + ] + }, + "safe-coerce": { + "type": "registry", + "version": "2.0.0", + "integrity": "sha256-EJrxtKt5xC7TYjBZrSBO/J7Aw3DmtilYjt4olpAXWVc=", + "dependencies": [ + "unsafe-coerce" + ] + }, + "spec": { + "type": "registry", + "version": "8.1.2", + "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=", + "dependencies": [ + "aff", + "ansi", + "arrays", + "avar", + "bifunctors", + "control", + "datetime", + "effect", + "either", + "exceptions", + "foldable-traversable", + "fork", + "identity", + "integers", + "lists", + "maybe", + "newtype", + "now", + "ordered-collections", + "parallel", + "pipes", + "prelude", + "refs", + "strings", + "tailrec", + "transformers", + "tuples" + ] + }, + "spec-node": { + "type": "registry", + "version": "0.0.3", + "integrity": "sha256-8/9+g0obbamV0YtqzmttbWs8HaWF7CEeW+eDtqzuAAo=", + "dependencies": [ + "aff", + "argonaut-codecs", + "argonaut-core", + "arrays", + "control", + "datetime", + "effect", + "either", + "foldable-traversable", + "identity", + "integers", + "maybe", + "newtype", + "node-buffer", + "node-fs", + "node-process", + "now", + "numbers", + "optparse", + "ordered-collections", + "partial", + "prelude", + "spec", + "strings", + "tuples" + ] + }, + "st": { + "type": "registry", + "version": "6.2.0", + "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=", + "dependencies": [ + "effect", + "partial", + "prelude", + "tailrec", + "unsafe-coerce" + ] + }, + "strings": { + "type": "registry", + "version": "6.0.1", + "integrity": "sha256-FNto2hoNW5Da6W6crIk77YXMagBvOGHZE1kO7eZ1vLM=", + "dependencies": [ + "arrays", + "control", + "either", + "enums", + "foldable-traversable", + "gen", + "integers", + "maybe", + "newtype", + "nonempty", + "partial", + "prelude", + "tailrec", + "tuples", + "unfoldable", + "unsafe-coerce" + ] + }, + "tailrec": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-xrorCfP0xV1wubs27idQCHQqdvSUuqmmL/am7Ji1wVU=", + "dependencies": [ + "bifunctors", + "effect", + "either", + "identity", + "maybe", + "partial", + "prelude", + "refs" + ] + }, + "transformers": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=", + "dependencies": [ + "control", + "distributive", + "effect", + "either", + "exceptions", + "foldable-traversable", + "identity", + "lazy", + "maybe", + "newtype", + "prelude", + "st", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "tuples": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-BKy7EfK8S1KeTCwyWqyW1wwcpSAQuh3IsczSVgS6fzY=", + "dependencies": [ + "control", + "invariant", + "prelude" + ] + }, + "type-equality": { + "type": "registry", + "version": "4.0.1", + "integrity": "sha256-BBqYOSnAKmayRvR5mtZoCf4SlYs2ipatD/+5nQx73aw=", + "dependencies": [] + }, + "typelevel-prelude": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-+pHi14/40mTj/+lmCWqL0T7iXIXD1+NSg/KItyyoB0A=", + "dependencies": [ + "prelude", + "type-equality" + ] + }, + "unfoldable": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-BK/6By1sKp/ztk+9CF3q43SV0QDTLVdsGqFerKHSmbg=", + "dependencies": [ + "foldable-traversable", + "maybe", + "partial", + "prelude", + "tuples" + ] + }, + "unsafe-coerce": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=", + "dependencies": [] + }, + "unsafe-reference": { + "type": "registry", + "version": "5.0.0", + "integrity": "sha256-ttSJTQUnK8AK2eGOsfRxLUJEDPL6pnmExjbiOqfwC6I=", + "dependencies": [ + "prelude" + ] + }, + "web-dom": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-rVeqkxChkjqisDvOujy9BMEgGqJkwX3t6g21bXfiG5o=", + "dependencies": [ + "effect", + "enums", + "maybe", + "newtype", + "nullable", + "prelude", + "unsafe-coerce", + "web-events" + ] + }, + "web-events": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-wZNeldG0OU+8qGycUJ9e6vxf1/GC4Ep4OlRMhvjs9LA=", + "dependencies": [ + "datetime", + "effect", + "enums", + "foreign", + "functions", + "maybe", + "newtype", + "nullable", + "prelude", + "unsafe-coerce" + ] + }, + "web-file": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-txsdkeWBKU/+5v9m5c6kysgJQSDyV1r7MHaeT4STpjQ=", + "dependencies": [ + "datetime", + "effect", + "enums", + "foreign", + "integers", + "maybe", + "media-types", + "nullable", + "numbers", + "partial", + "prelude", + "tuples", + "unfoldable", + "unsafe-coerce", + "web-events" + ] + }, + "web-html": { + "type": "registry", + "version": "4.1.1", + "integrity": "sha256-yOuZJGUxFrivHKpo/jKuKzFZaq6PPPE64ftSysXFssk=", + "dependencies": [ + "effect", + "enums", + "foreign", + "functions", + "js-date", + "maybe", + "media-types", + "newtype", + "nullable", + "prelude", + "unsafe-coerce", + "web-dom", + "web-events", + "web-file", + "web-storage" + ] + }, + "web-storage": { + "type": "registry", + "version": "5.0.0", + "integrity": "sha256-YJfos55oeKtBStNfoWfeflSy8j+4IDym5eaZtIY7YM4=", + "dependencies": [ + "effect", + "maybe", + "nullable", + "prelude", + "unsafe-coerce", + "web-events" + ] + } + } +} diff --git a/spago.test.dhall b/spago.test.dhall deleted file mode 100644 index 2272207..0000000 --- a/spago.test.dhall +++ /dev/null @@ -1,10 +0,0 @@ -let conf = ./spago.dhall - -let dependencies = [ "spec" ] - -let sources = [ "test/**/*.purs" ] - -in conf // - { dependencies = conf.dependencies # dependencies - , sources = conf.sources # sources - } diff --git a/spago.yaml b/spago.yaml new file mode 100644 index 0000000..65dce67 --- /dev/null +++ b/spago.yaml @@ -0,0 +1,38 @@ +package: + name: react-halo + publish: + version: 4.0.0 + license: BSD-3-Clause + location: + githubOwner: robertdp + githubRepo: purescript-react-halo + dependencies: + - aff + - arrays + - avar + - effect + - either + - foldable-traversable + - halogen-subscriptions + - maybe + - newtype + - ordered-collections + - prelude + - react-basic-hooks + - refs + - transformers + - tuples + - unsafe-reference + test: + main: Test.Main + dependencies: + - console + - control + - exceptions + - parallel + - react-basic-dom + - spec + - spec-node +workspace: + packageSet: + registry: 80.8.0 diff --git a/src/React/Halo.purs b/src/React/Halo.purs index c33e2c6..5ecdedd 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -3,7 +3,7 @@ module React.Halo ) where import React.Halo.Component (ComponentSpec, component) as Exports -import React.Halo.Hook (HookSpec, UseHalo(..), useHalo) as Exports -import React.Halo.Internal.Control (HaloAp, HaloM, fork, hoist, hoistAp, kill, props, subscribe, subscribe', unsubscribe) as Exports +import React.Halo.Hook (HaloHook, HookSpec, UseHalo(..), useHalo) as Exports +import React.Halo.Internal.Control (HaloM, fork, kill, props, subscribe, subscribe', unsubscribe) as Exports import React.Halo.Internal.Eval (EvalSpec, defaultEval, mkEval) as Exports -import React.Halo.Internal.Types (ForkId, Lifecycle(..), SubscriptionId) as Exports +import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, Lifecycle(..), SubscriptionId, TaskCounts, TaskPolicy(..), activityFor, activityTotals, emptyActivity) as Exports diff --git a/src/React/Halo/Component.purs b/src/React/Halo/Component.purs index 6991772..33f5408 100644 --- a/src/React/Halo/Component.purs +++ b/src/React/Halo/Component.purs @@ -1,26 +1,52 @@ -module React.Halo.Component where +module React.Halo.Component + ( ComponentSpec + , component + ) where import Prelude -import Data.Tuple.Nested ((/\)) import Effect (Effect) -import Effect.Aff (Aff) +import Effect.Aff (Error) import React.Basic.Hooks (Component, JSX) import React.Basic.Hooks as React import React.Halo.Hook (useHalo) -import React.Halo.Internal.Control (HaloM) -import React.Halo.Internal.Types (Lifecycle) +import React.Halo.Internal.Runtime (HaloM) +import React.Halo.Internal.Types (Activity, ErrorContext, Lifecycle, TaskPolicy) -type ComponentSpec props state action m = - { initialState :: props -> state - , eval :: Lifecycle props action -> HaloM props state action m Unit - , render :: { props :: props, state :: state, send :: action -> Effect Unit } -> JSX +type ComponentSpec props state action key = + { eval :: Lifecycle props action -> HaloM props state action key Unit + , initialState :: props -> state + , onError :: ErrorContext props action -> Error -> Effect Unit + , render :: + { activity :: Activity key + , dispatch :: action -> Effect Unit + , props :: props + , state :: state + } + -> JSX + , schedule :: action -> TaskPolicy key } --- | Build a component by providing a name and a Halo component spec. -component :: forall props state action. String -> ComponentSpec props state action Aff -> Component props -component name spec@{ eval, render } = +-- | Build a complete React component around a Halo action runtime. +component + :: forall props state action key + . Ord key + => String + -> ComponentSpec props state action key + -> Component props +component name spec = React.component name \props -> React.do initialState <- React.useMemo unit \_ -> spec.initialState props - state /\ send <- useHalo { props, initialState, eval } - pure (render { props, state, send }) + halo <- useHalo + { eval: spec.eval + , initialState + , onError: spec.onError + , props + , schedule: spec.schedule + } + pure $ spec.render + { activity: halo.activity + , dispatch: halo.dispatch + , props + , state: halo.state + } diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index ebce21c..c0ad10f 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -1,36 +1,87 @@ -module React.Halo.Hook where +module React.Halo.Hook + ( HaloHook + , HookSpec + , UseHalo(..) + , useHalo + ) where import Prelude + import Data.Newtype (class Newtype) -import Data.Tuple.Nested (type (/\), (/\)) +import Data.Tuple.Nested ((/\)) import Effect (Effect) -import Effect.Aff (Aff) +import Effect.Aff (Error) import Effect.Unsafe (unsafePerformEffect) import React.Basic.Hooks (Hook, UseEffect, UseMemo, UseState) import React.Basic.Hooks as React -import React.Halo.Internal.Control (HaloM) -import React.Halo.Internal.Eval (handleAction, handleUpdate, runFinalize, runInitialize) -import React.Halo.Internal.State (HaloState, createInitialState) -import React.Halo.Internal.Types (Lifecycle) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, syncSpec, updateProps) +import React.Halo.Internal.Types (Activity, ErrorContext, Lifecycle, TaskPolicy, emptyActivity) -type HookSpec props state action m = - { props :: props +-- | Configuration for `useHalo`. The task key is chosen by the application and +-- | only needs an `Ord` instance. +type HookSpec props state action key = + { eval :: Lifecycle props action -> HaloM props state action key Unit , initialState :: state - , eval :: Lifecycle props action -> HaloM props state action m Unit + , onError :: ErrorContext props action -> Error -> Effect Unit + , props :: props + , schedule :: action -> TaskPolicy key + } + +-- | Values exposed to rendering code. +type HaloHook state action key = + { activity :: Activity key + , dispatch :: action -> Effect Unit + , state :: state } -newtype UseHalo props state action hooks = UseHalo (UseEffect Unit (UseEffect Unit (UseMemo Unit (HaloState props state action) (UseState state hooks)))) +newtype UseHalo props state action key hooks = UseHalo + ( UseEffect Unit + ( UseEffect Unit + ( UseEffect Unit + ( UseMemo Unit (Runtime props state action key) + ( UseState (Activity key) + (UseState state hooks) + ) + ) + ) + ) + ) -derive instance newtypeUseHalo :: Newtype (UseHalo props state action hooks) _ +derive instance newtypeUseHalo :: Newtype (UseHalo props state action key hooks) _ --- | Run renderless Halo in the current component. This allows Halo to be used with other hooks and other ways of --- | building components. -useHalo :: forall props state action. HookSpec props state action Aff -> Hook (UseHalo props state action) (state /\ (action -> Effect Unit)) -useHalo { props, initialState, eval } = +-- | Run Halo inside a `react-basic-hooks` component. +useHalo + :: forall props state action key + . Ord key + => HookSpec props state action key + -> Hook (UseHalo props state action key) (HaloHook state action key) +useHalo { props, initialState, eval, schedule, onError } = React.coerceHook React.do state /\ setState <- React.useState' initialState - halo <- React.useMemo unit \_ -> unsafePerformEffect $ - createInitialState { props, state: initialState, eval, update: setState } - React.useEffectOnce (runInitialize halo *> pure (runFinalize halo)) - React.useEffectAlways (handleUpdate halo props *> mempty) - pure (state /\ handleAction halo) + activity /\ setActivity <- React.useState' emptyActivity + runtime <- React.useMemo unit \_ -> unsafePerformEffect $ + createRuntime + { activityUpdate: setActivity + , initialProps: props + , initialState + , spec: { eval, schedule, onError } + , stateUpdate: setState + } + React.useEffectAlways do + syncSpec runtime + { activityUpdate: setActivity + , spec: { eval, schedule, onError } + , stateUpdate: setState + } + pure mempty + React.useEffectOnce do + activate runtime + pure (deactivate runtime) + React.useEffectAlways do + updateProps runtime props + pure mempty + pure + { activity + , dispatch: dispatch runtime + , state + } diff --git a/src/React/Halo/Internal/Control.purs b/src/React/Halo/Internal/Control.purs index 6627fde..a11f24c 100644 --- a/src/React/Halo/Internal/Control.purs +++ b/src/React/Halo/Internal/Control.purs @@ -1,167 +1,52 @@ -module React.Halo.Internal.Control where +module React.Halo.Internal.Control + ( HaloM + , fork + , kill + , props + , subscribe + , subscribe' + , unsubscribe + ) where import Prelude -import Control.Applicative.Free (FreeAp, hoistFreeAp, liftFreeAp) -import Control.Monad.Error.Class (class MonadThrow, throwError) -import Control.Monad.Free (Free, hoistFree, liftF) -import Control.Monad.Reader (class MonadAsk, ask) -import Control.Monad.Rec.Class (class MonadRec, Step(..), tailRecM) -import Control.Monad.State (class MonadState) -import Control.Monad.Trans.Class (class MonadTrans, lift) -import Control.Monad.Writer (class MonadTell, tell) -import Control.Parallel (class Parallel) -import Data.Bifunctor (lmap) -import Data.Tuple (Tuple) -import Effect.Aff.Class (class MonadAff, liftAff) -import Effect.Class (class MonadEffect, liftEffect) + import Halogen.Subscription (Emitter) +import React.Halo.Internal.Runtime as Runtime import React.Halo.Internal.Types (ForkId, SubscriptionId) --- | The Halo evaluation algebra --- | --- | - `props` are the component props --- | - `state` is the component state --- | - `action` is the set of actions that the component handles --- | - `m` is the monad used during evaluation --- | - `a` is the result type -data HaloF props state action (m :: Type -> Type) a - = Props (props -> a) - | State (state -> Tuple a state) - | Subscribe (SubscriptionId -> Emitter action) (SubscriptionId -> a) - | Unsubscribe SubscriptionId a - | Lift (m a) - | Par (HaloAp props state action m a) - | Fork (HaloM props state action m Unit) (ForkId -> a) - | Kill ForkId a - -instance functorHaloF :: Functor m => Functor (HaloF props state action m) where - map f = case _ of - Props k -> Props (f <<< k) - State k -> State (lmap f <<< k) - Subscribe fes k -> Subscribe fes (map f k) - Unsubscribe sid a -> Unsubscribe sid (f a) - Lift m -> Lift (map f m) - Par par -> Par (map f par) - Fork m k -> Fork m (map f k) - Kill fid a -> Kill fid (f a) - --- | The Halo evaluation monad. It lifts the `HaloF` algebra into a free monad. --- | --- | - `props` are the component props --- | - `state` is the component state --- | - `action` is the set of actions that the component handles --- | - `m` is the monad used during evaluation --- | - `a` is the result type -newtype HaloM props state action (m :: Type -> Type) a = HaloM (Free (HaloF props state action m) a) - -derive newtype instance functorHaloM :: Functor (HaloM props state action m) - -derive newtype instance applyHaloM :: Apply (HaloM props state action m) - -derive newtype instance applicativeHaloM :: Applicative (HaloM props state action m) - -derive newtype instance bindHaloM :: Bind (HaloM props state action m) - -derive newtype instance monadHaloM :: Monad (HaloM props state action m) - -derive newtype instance semigroupHaloM :: Semigroup a => Semigroup (HaloM props state action m a) - -derive newtype instance monoidHaloM :: Monoid a => Monoid (HaloM props state action m a) - -instance monadTransHaloM :: MonadTrans (HaloM props state action) where - lift = HaloM <<< liftF <<< Lift - -instance monadEffectHaloM :: MonadEffect m => MonadEffect (HaloM props state action m) where - liftEffect = lift <<< liftEffect - -instance monadAffHaloM :: MonadAff m => MonadAff (HaloM props state action m) where - liftAff = lift <<< liftAff - -instance monadStateHaloM :: MonadState state (HaloM props state action m) where - state = HaloM <<< liftF <<< State - -instance monadRecHaloM :: MonadRec (HaloM props state action m) where - tailRecM k a = - k a - >>= case _ of - Loop x -> tailRecM k x - Done y -> pure y - -instance monadAskHaloM :: MonadAsk r m => MonadAsk r (HaloM props state action m) where - ask = lift ask - -instance monadTellHaloM :: MonadTell w m => MonadTell w (HaloM props state action m) where - tell = lift <<< tell - -instance monadThrowHaloM :: MonadThrow e m => MonadThrow e (HaloM props state action m) where - throwError = lift <<< throwError - --- | The Halo parallel evaluation applicative. It lifts `HaloM` into a free applicative. --- | --- | - `props` are the component props --- | - `state` is the component state --- | - `action` is the set of actions that the component handles --- | - `m` is the monad used during evaluation --- | - `a` is the result type -newtype HaloAp props state action (m :: Type -> Type) a = HaloAp (FreeAp (HaloM props state action m) a) - -derive newtype instance functorHaloAp :: Functor (HaloAp props state action m) - -derive newtype instance applyHaloAp :: Apply (HaloAp props state action m) - -derive newtype instance applicativeHaloAp :: Applicative (HaloAp props state action m) - -instance parallelHaloM :: Parallel (HaloAp props state action m) (HaloM props state action m) where - parallel = HaloAp <<< liftFreeAp - sequential = HaloM <<< liftF <<< Par - --- | Hoist (transform) the base monad of a `HaloM` expression. -hoist :: forall props state action m m'. Functor m => (m ~> m') -> HaloM props state action m ~> HaloM props state action m' -hoist nat (HaloM component) = HaloM (hoistFree go component) - where - go :: HaloF props state action m ~> HaloF props state action m' - go = case _ of - Props k -> Props k - State k -> State k - Subscribe event k -> Subscribe event k - Unsubscribe sid a -> Unsubscribe sid a - Lift m -> Lift (nat m) - Par par -> Par (hoistAp nat par) - Fork m k -> Fork (hoist nat m) k - Kill fid a -> Kill fid a - --- | Hoist (transform) the base applicative of a `HaloAp` expression. -hoistAp :: forall props state action m m'. Functor m => (m ~> m') -> HaloAp props state action m ~> HaloAp props state action m' -hoistAp nat (HaloAp component) = HaloAp (hoistFreeAp (hoist nat) component) - --- | Read the current props. -props :: forall props state action m. HaloM props state action m props -props = HaloM (liftF (Props identity)) - --- | Subscribe to new actions from an `Emitter`. Subscriptions will be automatically cancelled when the component --- | unmounts. --- | --- | Returns a `SubscriptionId` which can be used with `unsubscribe` to manually cancel a subscription. -subscribe :: forall props state action m. Emitter action -> HaloM props state action m SubscriptionId -subscribe = subscribe' <<< const - --- | Same as `subscribe` but the event-producing logic is also passed the `SuscriptionId`. This is useful when events --- | need to unsubscribe themselves. -subscribe' :: forall props state action m. (SubscriptionId -> Emitter action) -> HaloM props state action m SubscriptionId -subscribe' event = HaloM (liftF (Subscribe event identity)) - --- | Cancels the event subscription belonging to the `SubscriptionId`. -unsubscribe :: forall props state action m. SubscriptionId -> HaloM props state action m Unit -unsubscribe sid = HaloM (liftF (Unsubscribe sid unit)) - --- | Start a `HaloM` process running independantly from the current "thread". Forks are tracked automatically and --- | killed when the `Finalize` event occurs (when the component unmounts). New forks can still be created during the --- | `Finalize` event, but once evaluation ends there will be no way of killing them. --- | --- | Returns a `ForkId` for the new process. -fork :: forall props state action m. HaloM props state action m Unit -> HaloM props state action m ForkId -fork m = HaloM (liftF (Fork m identity)) - --- | Kills the process belonging to the `ForkId`. -kill :: forall props state action m. ForkId -> HaloM props state action m Unit -kill fid = HaloM (liftF (Kill fid unit)) +type HaloM props state action key = Runtime.HaloM props state action key + +props :: forall props state action key. HaloM props state action key props +props = Runtime.props + +subscribe + :: forall props state action key + . Ord key + => Emitter action + -> HaloM props state action key SubscriptionId +subscribe = Runtime.subscribe + +subscribe' + :: forall props state action key + . Ord key + => (SubscriptionId -> Emitter action) + -> HaloM props state action key SubscriptionId +subscribe' = Runtime.subscribe' + +unsubscribe + :: forall props state action key + . SubscriptionId + -> HaloM props state action key Unit +unsubscribe = Runtime.unsubscribe + +fork + :: forall props state action key + . HaloM props state action key Unit + -> HaloM props state action key ForkId +fork = Runtime.fork + +kill + :: forall props state action key + . ForkId + -> HaloM props state action key Unit +kill = Runtime.kill diff --git a/src/React/Halo/Internal/Eval.purs b/src/React/Halo/Internal/Eval.purs index 6258aca..8e8ffb9 100644 --- a/src/React/Halo/Internal/Eval.purs +++ b/src/React/Halo/Internal/Eval.purs @@ -1,138 +1,38 @@ -module React.Halo.Internal.Eval where +module React.Halo.Internal.Eval + ( EvalSpec + , defaultEval + , mkEval + ) where import Prelude -import Control.Applicative.Free (foldFreeAp) -import Control.Monad.Free (foldFree) -import Data.Either (either) import Data.Foldable (traverse_) -import Data.Map as Map import Data.Maybe (Maybe(..)) -import Data.Tuple (Tuple(..)) -import Effect (Effect) -import Effect.Aff (Aff, ParAff, finally, parallel, sequential, throwError) -import Effect.Aff as Aff -import Effect.Class (liftEffect) -import Effect.Ref as Ref -import Halogen.Subscription (subscribe, unsubscribe) -import React.Halo.Internal.Control (HaloAp(..), HaloF(..), HaloM(..)) -import React.Halo.Internal.State (HaloState(..)) -import React.Halo.Internal.State as State -import React.Halo.Internal.Types (ForkId(..), Lifecycle(..), SubscriptionId(..)) -import Unsafe.Reference (unsafeRefEq) +import React.Halo.Internal.Runtime (HaloM) +import React.Halo.Internal.Types (Lifecycle(..)) --- | Interprets `HaloM` into the base monad `Aff` for asynchronous effects. -evalHaloM :: forall props state action. HaloState props state action -> HaloM props state action Aff ~> Aff -evalHaloM hs (HaloM halo) = foldFree (evalHaloF hs) halo - --- | Interprets `HaloAp` into the base applicative `ParAff` for parallel effects. -evalHaloAp :: forall props state action. HaloState props state action -> HaloAp props state action Aff ~> ParAff -evalHaloAp hs (HaloAp halo) = foldFreeAp (parallel <<< evalHaloM hs) halo - --- | Interprets `HaloF` into the base monad `Aff`, keeping track of state in `HaloState`. -evalHaloF :: forall props state action. HaloState props state action -> HaloF props state action Aff ~> Aff -evalHaloF hs@(HaloState s) = case _ of - Props k -> - liftEffect do - props <- Ref.read s.props - pure (k props) - State f -> - liftEffect do - state <- Ref.read s.state - case f state of - Tuple a state' - | not unsafeRefEq state state' -> do - Ref.write state' s.state - s.update state' - pure a - | otherwise -> pure a - Subscribe sub k -> - liftEffect do - sid <- State.fresh SubscriptionId hs - unlessM (Ref.read s.finalized) do - canceller <- subscribe (sub sid) (handleAction hs) - Ref.modify_ (Map.insert sid canceller) s.subscriptions - pure (k sid) - Unsubscribe sid a -> - liftEffect do - subscription <- Map.lookup sid <$> Ref.read s.subscriptions - traverse_ unsubscribe subscription - pure a - Lift m -> - m - Par p -> - sequential (evalHaloAp hs p) - Fork fh k -> - liftEffect do - fid <- State.fresh ForkId hs - doneRef <- Ref.new false - fiber <- - Aff.launchAff - $ finally - ( liftEffect do - Ref.modify_ (Map.delete fid) s.forks - Ref.write true doneRef - ) - (evalHaloM hs fh) - unlessM (Ref.read doneRef) do - Ref.modify_ (Map.insert fid fiber) s.forks - pure (k fid) - Kill fid a -> do - forks <- liftEffect (Ref.read s.forks) - traverse_ (Aff.killFiber (Aff.error "Cancelled")) (Map.lookup fid forks) - pure a - --- | A simpler interface for building the components eval function. The main lifecycle events map directly into --- | actions, so only the action handling logic needs to be written using `HaloM`. -type EvalSpec props state action m = - { handleAction :: action -> HaloM props state action m Unit +-- | Convenience configuration for routing lifecycle events into the same action +-- | handler used by dispatched actions. +type EvalSpec props state action key = + { handleAction :: action -> HaloM props state action key Unit , initialize :: Maybe action , update :: props -> Maybe action - , finalize :: Maybe action } --- | The empty `EvalSpec`. -defaultEval :: forall props action state m. EvalSpec props state action m +-- | An evaluator that ignores activation and prop updates until configured. +defaultEval :: forall props state action key. EvalSpec props state action key defaultEval = { handleAction: \_ -> pure unit , initialize: Nothing , update: \_ -> Nothing - , finalize: Nothing } --- | Given an `EvalSpec` builder, it will return an eval function. -mkEval :: forall props state action m. EvalSpec props state action m -> Lifecycle props action -> HaloM props state action m Unit +mkEval + :: forall props state action key + . EvalSpec props state action key + -> Lifecycle props action + -> HaloM props state action key Unit mkEval eval = case _ of - Initialize -> traverse_ eval.handleAction eval.initialize - Update props -> traverse_ eval.handleAction $ eval.update props + Activate -> traverse_ eval.handleAction eval.initialize + Update previousProps -> traverse_ eval.handleAction (eval.update previousProps) Action action -> eval.handleAction action - Finalize -> traverse_ eval.handleAction eval.finalize - --- | Simple way to run Aff logic asynchronously, while bringing errors back into Effect. -runAff :: Aff Unit -> Effect Unit -runAff = Aff.runAff_ (either throwError pure) - -runInitialize :: forall props state action. HaloState props action state -> Effect Unit -runInitialize hs@(HaloState s) = - runAff $ evalHaloM hs $ s.eval Initialize - -handleUpdate :: forall props state action. HaloState props action state -> props -> Effect Unit -handleUpdate hs@(HaloState s) newProps = do - prevProps <- Ref.read s.props - unless (unsafeRefEq newProps prevProps) do - Ref.write newProps s.props - runAff $ evalHaloM hs $ s.eval $ Update prevProps - -handleAction :: forall props state action. HaloState props state action -> action -> Effect Unit -handleAction hs@(HaloState s) action = - unlessM (Ref.read s.finalized) do - runAff $ evalHaloM hs $ s.eval $ Action action - -runFinalize :: forall props state action. HaloState props state action -> Effect Unit -runFinalize hs@(HaloState s) = do - Ref.write true s.finalized - subscriptions <- Ref.modify' (\s' -> { state: Map.empty, value: s' }) s.subscriptions - traverse_ unsubscribe (Map.values subscriptions) - forks <- Ref.modify' (\s' -> { state: Map.empty, value: s' }) s.forks - traverse_ (runAff <<< Aff.killFiber (Aff.error "Cancelled")) (Map.values forks) - runAff $ evalHaloM hs $ s.eval Finalize diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs new file mode 100644 index 0000000..74c9ea3 --- /dev/null +++ b/src/React/Halo/Internal/Runtime.purs @@ -0,0 +1,620 @@ +module React.Halo.Internal.Runtime + ( HaloM + , Runtime + , RuntimeSpec + , activate + , createRuntime + , deactivate + , dispatch + , fork + , kill + , props + , runForTest + , subscribe + , subscribe' + , syncSpec + , unsubscribe + , updateProps + ) where + +import Prelude + +import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.State.Class (class MonadState) +import Data.Array as Array +import Data.Either (Either(..)) +import Data.Foldable (and, foldl, traverse_) +import Data.Map (Map) +import Data.Map as Map +import Data.Maybe (Maybe(..), maybe) +import Data.Traversable (traverse) +import Data.Tuple (Tuple(..)) +import Effect (Effect) +import Effect.Aff (Aff, Error, Fiber) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (class MonadAff, liftAff) +import Effect.AVar as EffectAVar +import Effect.Class (class MonadEffect, liftEffect) +import Effect.Ref (Ref) +import Effect.Ref as Ref +import Halogen.Subscription (Emitter, Subscription) +import Halogen.Subscription as HS +import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), Lifecycle(..), SubscriptionId(..), TaskPolicy(..), emptyActivity) +import Unsafe.Reference (unsafeRefEq) + +-- | The direct Halo evaluator. Its environment is intentionally private so a +-- | computation can only obtain the capabilities exported by `React.Halo`. +newtype HaloM props state action key a = HaloM + (ReaderT (Execution props state action key) Aff a) + +derive newtype instance functorHaloM :: Functor (HaloM props state action key) +derive newtype instance applyHaloM :: Apply (HaloM props state action key) +derive newtype instance applicativeHaloM :: Applicative (HaloM props state action key) +derive newtype instance bindHaloM :: Bind (HaloM props state action key) +derive newtype instance monadHaloM :: Monad (HaloM props state action key) +derive newtype instance monadEffectHaloM :: MonadEffect (HaloM props state action key) +derive newtype instance monadAffHaloM :: MonadAff (HaloM props state action key) + +type RuntimeSpec props state action key = + { eval :: Lifecycle props action -> HaloM props state action key Unit + , onError :: ErrorContext props action -> Error -> Effect Unit + , schedule :: action -> TaskPolicy key + } + +newtype Runtime props state action key = Runtime + { activityUpdate :: Ref (Activity key -> Effect Unit) + , fresh :: Ref Int + , props :: Ref props + , scope :: Ref (Maybe (Scope props state action key)) + , spec :: Ref (RuntimeSpec props state action key) + , state :: Ref state + , stateUpdate :: Ref (state -> Effect Unit) + } + +newtype Scope :: Type -> Type -> Type -> Type -> Type +newtype Scope props state action key = Scope + { active :: Ref Boolean + , every :: Ref (Map Int (Root props state action key)) + , generation :: Int + , roots :: Ref (Map Int (Root props state action key)) + , subscriptions :: Ref (Map SubscriptionId Subscription) + , tasks :: Ref (Map key (TaskSlot props state action key)) + } + +type TaskSlot :: Type -> Type -> Type -> Type -> Type +type TaskSlot props state action key = + { queued :: Array action + , running :: Map Int (Root props state action key) + } + +newtype Owner :: Type -> Type -> Type -> Type -> Type +newtype Owner props state action key = Owner + { alive :: Ref Boolean + , children :: Ref (Map ForkId (Root props state action key)) + , lineage :: Array (Ref Boolean) + } + +newtype Root :: Type -> Type -> Type -> Type -> Type +newtype Root props state action key = Root + { fiber :: Fiber Unit + , owner :: Owner props state action key + } + +type Execution props state action key = + { context :: ErrorContext props action + , owner :: Owner props state action key + , runtime :: Runtime props state action key + , scope :: Scope props state action key + } + +type Prepared :: Type -> Type -> Type -> Type -> Type +type Prepared props state action key = + { root :: Root props state action key + , start :: Effect Unit + } + +instance monadStateHaloM :: MonadState state (HaloM props state action key) where + state f = HaloM do + execution <- ask + liftEffect do + current <- isCurrent execution + if current then do + let Runtime runtime = execution.runtime + oldState <- Ref.read runtime.state + let Tuple result newState = f oldState + unless (unsafeRefEq oldState newState) do + Ref.write newState runtime.state + update <- Ref.read runtime.stateUpdate + update newState + pure result + else do + let Runtime runtime = execution.runtime + Tuple result _ <- f <$> Ref.read runtime.state + pure result + +createRuntime + :: forall props state action key + . { activityUpdate :: Activity key -> Effect Unit + , initialProps :: props + , initialState :: state + , spec :: RuntimeSpec props state action key + , stateUpdate :: state -> Effect Unit + } + -> Effect (Runtime props state action key) +createRuntime input = do + activityUpdate <- Ref.new input.activityUpdate + freshRef <- Ref.new 0 + propsRef <- Ref.new input.initialProps + scope <- Ref.new Nothing + spec <- Ref.new input.spec + state <- Ref.new input.initialState + stateUpdate <- Ref.new input.stateUpdate + pure $ Runtime + { activityUpdate + , fresh: freshRef + , props: propsRef + , scope + , spec + , state + , stateUpdate + } + +syncSpec + :: forall props state action key + . Runtime props state action key + -> { activityUpdate :: Activity key -> Effect Unit + , spec :: RuntimeSpec props state action key + , stateUpdate :: state -> Effect Unit + } + -> Effect Unit +syncSpec (Runtime runtime) input = do + Ref.write input.activityUpdate runtime.activityUpdate + Ref.write input.spec runtime.spec + Ref.write input.stateUpdate runtime.stateUpdate + +activate :: forall props state action key. Ord key => Runtime props state action key -> Effect Unit +activate runtime@(Runtime state) = do + activeScope <- Ref.read state.scope + case activeScope of + Just _ -> pure unit + Nothing -> do + generation <- fresh runtime + active <- Ref.new true + every <- Ref.new Map.empty + roots <- Ref.new Map.empty + subscriptions <- Ref.new Map.empty + tasks <- Ref.new Map.empty + let scope = Scope { active, every, generation, roots, subscriptions, tasks } + Ref.write (Just scope) state.scope + spec <- Ref.read state.spec + startLifecycle runtime scope ActivationError (spec.eval Activate) + +deactivate :: forall props state action key. Ord key => Runtime props state action key -> Effect Unit +deactivate runtime@(Runtime state) = do + activeScope <- Ref.read state.scope + case activeScope of + Nothing -> pure unit + Just (Scope current) -> do + Ref.write false current.active + Ref.write Nothing state.scope + + roots <- takeRef current.roots Map.empty + every <- takeRef current.every Map.empty + tasks <- takeRef current.tasks Map.empty + subscriptions <- takeRef current.subscriptions Map.empty + + publishActivity runtime emptyActivity + traverse_ HS.unsubscribe (Map.values subscriptions) + traverse_ cancelRoot (Map.values roots) + traverse_ cancelRoot (Map.values every) + traverse_ (traverse_ cancelRoot <<< Map.values <<< _.running) (Map.values tasks) + +updateProps + :: forall props state action key + . Ord key + => Runtime props state action key + -> props + -> Effect Unit +updateProps runtime@(Runtime state) newProps = do + previousProps <- Ref.read state.props + unless (unsafeRefEq previousProps newProps) do + Ref.write newProps state.props + activeScope <- Ref.read state.scope + traverse_ + ( \scope -> do + spec <- Ref.read state.spec + startLifecycle runtime scope (UpdateError previousProps) (spec.eval (Update previousProps)) + ) + activeScope + +dispatch + :: forall props state action key + . Ord key + => Runtime props state action key + -> action + -> Effect Unit +dispatch runtime@(Runtime state) action = do + activeScope <- Ref.read state.scope + traverse_ (\scope -> dispatchToScope runtime scope action) activeScope + +dispatchToScope + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> action + -> Effect Unit +dispatchToScope runtime@(Runtime state) scope action = do + current <- isScopeCurrent runtime scope + when current do + spec <- Ref.read state.spec + schedule runtime scope (spec.schedule action) action + +props :: forall props state action key. HaloM props state action key props +props = HaloM do + execution <- ask + let Runtime runtime = execution.runtime + liftEffect $ Ref.read runtime.props + +subscribe + :: forall props state action key + . Ord key + => Emitter action + -> HaloM props state action key SubscriptionId +subscribe = subscribe' <<< const + +subscribe' + :: forall props state action key + . Ord key + => (SubscriptionId -> Emitter action) + -> HaloM props state action key SubscriptionId +subscribe' makeEmitter = HaloM do + execution <- ask + liftEffect do + sid <- SubscriptionId <$> fresh execution.runtime + current <- isCurrent execution + when current do + let Scope scope = execution.scope + subscription <- HS.subscribe (makeEmitter sid) (dispatchToScope execution.runtime execution.scope) + Ref.modify_ (Map.insert sid subscription) scope.subscriptions + pure sid + +unsubscribe + :: forall props state action key + . SubscriptionId + -> HaloM props state action key Unit +unsubscribe sid = HaloM do + execution <- ask + liftEffect do + current <- isCurrent execution + when current do + let Scope scope = execution.scope + subscription <- Ref.modify' + ( \subscriptions -> + { state: Map.delete sid subscriptions + , value: Map.lookup sid subscriptions + } + ) + scope.subscriptions + traverse_ HS.unsubscribe subscription + +fork + :: forall props state action key + . HaloM props state action key Unit + -> HaloM props state action key ForkId +fork child = HaloM do + execution <- ask + liftEffect do + fid <- ForkId <$> fresh execution.runtime + current <- isCurrent execution + when current do + prepared <- prepare (Just execution.owner) execution.runtime execution.scope execution.context child \_ -> do + let Owner parent = execution.owner + Ref.modify_ (Map.delete fid) parent.children + let Owner parent = execution.owner + Ref.modify_ (Map.insert fid prepared.root) parent.children + prepared.start + pure fid + +kill + :: forall props state action key + . ForkId + -> HaloM props state action key Unit +kill fid = HaloM do + execution <- ask + let Owner parent = execution.owner + child <- liftEffect $ Ref.modify' + ( \children -> + { state: Map.delete fid children + , value: Map.lookup fid children + } + ) + parent.children + traverse_ (liftAff <<< cancelRootAff) child + +-- | Internal test seam: run one computation in a currently active scope. This +-- | is not re-exported by `React.Halo`. +runForTest + :: forall props state action key + . Ord key + => Runtime props state action key + -> ErrorContext props action + -> HaloM props state action key Unit + -> Effect Unit +runForTest runtime@(Runtime state) context computation = do + activeScope <- Ref.read state.scope + traverse_ (\scope -> startLifecycle runtime scope context computation) activeScope + +startLifecycle + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> ErrorContext props action + -> HaloM props state action key Unit + -> Effect Unit +startLifecycle runtime scope@(Scope current) context computation = do + runId <- fresh runtime + prepared <- prepare Nothing runtime scope context computation \_ -> + Ref.modify_ (Map.delete runId) current.roots + Ref.modify_ (Map.insert runId prepared.root) current.roots + prepared.start + +schedule + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> TaskPolicy key + -> action + -> Effect Unit +schedule runtime scope@(Scope current) policy action = case policy of + Every -> do + runId <- fresh runtime + prepared <- prepare Nothing runtime scope (ActionError action) (evalAction runtime action) \_ -> do + Ref.modify_ (Map.delete runId) current.every + notifyActivity runtime scope + Ref.modify_ (Map.insert runId prepared.root) current.every + notifyActivity runtime scope + prepared.start + Restartable key -> do + tasks <- Ref.read current.tasks + let previous = maybe mempty (Map.values <<< _.running) (Map.lookup key tasks) + Ref.modify_ (Map.insert key { running: Map.empty, queued: [] }) current.tasks + traverse_ cancelRoot previous + startKeyed runtime scope key action + Drop key -> do + tasks <- Ref.read current.tasks + let busy = maybe false (\slot -> not Map.isEmpty slot.running || not Array.null slot.queued) (Map.lookup key tasks) + unless busy $ startKeyed runtime scope key action + Enqueue key -> enqueueOrStart runtime scope key action false + KeepLatest key -> enqueueOrStart runtime scope key action true + +startKeyed + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> key + -> action + -> Effect Unit +startKeyed runtime scope@(Scope current) key action = do + runId <- fresh runtime + prepared <- prepare Nothing runtime scope (ActionError action) (evalAction runtime action) \_ -> + completeKeyed runtime scope key runId + Ref.modify_ (Map.alter (Just <<< addRun runId prepared.root <<< maybe emptySlot identity) key) current.tasks + notifyActivity runtime scope + prepared.start + +enqueueOrStart + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> key + -> action + -> Boolean + -> Effect Unit +enqueueOrStart runtime scope@(Scope current) key action keepOnlyLatest = do + tasks <- Ref.read current.tasks + case Map.lookup key tasks of + Just slot | not Map.isEmpty slot.running -> do + let queued = if keepOnlyLatest then [ action ] else Array.snoc slot.queued action + Ref.modify_ (Map.insert key (slot { queued = queued })) current.tasks + notifyActivity runtime scope + _ -> startKeyed runtime scope key action + +completeKeyed + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> key + -> Int + -> Effect Unit +completeKeyed runtime scope@(Scope current) key runId = do + tasks <- Ref.read current.tasks + case Map.lookup key tasks of + Nothing -> pure unit + Just slot | not (Map.member runId slot.running) -> pure unit + Just slot -> do + let running = Map.delete runId slot.running + case Array.uncons slot.queued of + Just { head, tail } | Map.isEmpty running -> do + Ref.modify_ (Map.insert key { running, queued: tail }) current.tasks + notifyActivity runtime scope + startKeyed runtime scope key head + _ -> do + if Map.isEmpty running && Array.null slot.queued then + Ref.modify_ (Map.delete key) current.tasks + else + Ref.modify_ (Map.insert key (slot { running = running })) current.tasks + notifyActivity runtime scope + +prepare + :: forall props state action key + . Maybe (Owner props state action key) + -> Runtime props state action key + -> Scope props state action key + -> ErrorContext props action + -> HaloM props state action key Unit + -> (Owner props state action key -> Effect Unit) + -> Effect (Prepared props state action key) +prepare parent runtime scope context computation onComplete = do + owner <- createOwner parent + gate <- EffectAVar.empty + fiber <- Aff.launchAff $ do + void $ AVar.take gate + Aff.finally + (closeOwner owner *> liftEffect (onComplete owner)) + do + outcome <- Aff.attempt $ runHaloM { context, owner, runtime, scope } computation + case outcome of + Left error -> do + current <- liftEffect $ isCurrent { context, owner, runtime, scope } + when current do + let Runtime state = runtime + spec <- liftEffect $ Ref.read state.spec + liftEffect $ spec.onError context error + Right _ -> pure unit + let root = Root { fiber, owner } + pure + { root + , start: Aff.launchAff_ (AVar.put unit gate) + } + +runHaloM + :: forall props state action key a + . Execution props state action key + -> HaloM props state action key a + -> Aff a +runHaloM execution (HaloM computation) = runReaderT computation execution + +evalAction + :: forall props state action key + . Runtime props state action key + -> action + -> HaloM props state action key Unit +evalAction (Runtime runtime) action = HaloM do + spec <- liftEffect $ Ref.read runtime.spec + let HaloM computation = spec.eval (Action action) + computation + +createOwner + :: forall props state action key + . Maybe (Owner props state action key) + -> Effect (Owner props state action key) +createOwner parent = do + alive <- Ref.new true + children <- Ref.new Map.empty + let + ancestors = case parent of + Just (Owner owner) -> owner.lineage + Nothing -> [] + pure $ Owner { alive, children, lineage: Array.cons alive ancestors } + +closeOwner :: forall props state action key. Owner props state action key -> Aff Unit +closeOwner (Owner owner) = do + children <- liftEffect do + Ref.write false owner.alive + takeRef owner.children Map.empty + traverse_ cancelRootAff (Map.values children) + +cancelRoot :: forall props state action key. Root props state action key -> Effect Unit +cancelRoot root@(Root current) = do + let Owner owner = current.owner + -- Fence commits synchronously. Fiber cancellation is asynchronous and cannot + -- stop external effects that have already happened. + Ref.write false owner.alive + Aff.launchAff_ (cancelRootAff root) + +cancelRootAff :: forall props state action key. Root props state action key -> Aff Unit +cancelRootAff (Root root) = do + let Owner owner = root.owner + liftEffect $ Ref.write false owner.alive + Aff.killFiber (Aff.error "Halo scope cancelled") root.fiber + +isCurrent + :: forall props state action key + . Execution props state action key + -> Effect Boolean +isCurrent execution = do + let Owner owner = execution.owner + let Scope scope = execution.scope + let Runtime runtime = execution.runtime + ownerAlive <- and <$> traverse Ref.read owner.lineage + scopeActive <- Ref.read scope.active + activeScope <- Ref.read runtime.scope + pure $ ownerAlive && scopeActive && case activeScope of + Just (Scope active) -> active.generation == scope.generation + Nothing -> false + +notifyActivity + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> Effect Unit +notifyActivity runtime@(Runtime state) scope@(Scope current) = do + active <- isScopeCurrent runtime scope + when active do + every <- Ref.read current.every + tasks <- Ref.read current.tasks + let + counts slot = + { running: Map.size slot.running + , queued: Array.length slot.queued + } + byKey = map counts tasks + keyed = foldl addCounts { running: 0, queued: 0 } (Map.values byKey) + activity = Activity + { total: keyed { running = keyed.running + Map.size every } + , byKey + } + update <- Ref.read state.activityUpdate + update activity + +publishActivity + :: forall props state action key + . Runtime props state action key + -> Activity key + -> Effect Unit +publishActivity (Runtime runtime) activity = do + update <- Ref.read runtime.activityUpdate + update activity + +isScopeCurrent + :: forall props state action key + . Runtime props state action key + -> Scope props state action key + -> Effect Boolean +isScopeCurrent (Runtime runtime) (Scope scope) = do + scopeActive <- Ref.read scope.active + activeScope <- Ref.read runtime.scope + pure $ scopeActive && case activeScope of + Just (Scope active) -> active.generation == scope.generation + Nothing -> false + +addCounts :: { running :: Int, queued :: Int } -> { running :: Int, queued :: Int } -> { running :: Int, queued :: Int } +addCounts left right = + { running: left.running + right.running + , queued: left.queued + right.queued + } + +emptySlot :: forall props state action key. TaskSlot props state action key +emptySlot = { queued: [], running: Map.empty } + +addRun + :: forall props state action key + . Int + -> Root props state action key + -> TaskSlot props state action key + -> TaskSlot props state action key +addRun runId root slot = slot { running = Map.insert runId root slot.running } + +fresh :: forall props state action key. Runtime props state action key -> Effect Int +fresh (Runtime runtime) = Ref.modify' (\value -> { state: value + 1, value }) runtime.fresh + +takeRef :: forall a. Ref a -> a -> Effect a +takeRef ref replacement = Ref.modify' (\value -> { state: replacement, value }) ref diff --git a/src/React/Halo/Internal/State.purs b/src/React/Halo/Internal/State.purs deleted file mode 100644 index 6185267..0000000 --- a/src/React/Halo/Internal/State.purs +++ /dev/null @@ -1,46 +0,0 @@ -module React.Halo.Internal.State where - -import Prelude -import Data.Map (Map) -import Data.Map as Map -import Effect (Effect) -import Effect.Aff (Aff, Fiber) -import Effect.Ref (Ref) -import Effect.Ref as Ref -import Halogen.Subscription (Subscription) -import React.Halo.Internal.Control (HaloM) -import React.Halo.Internal.Types (ForkId, Lifecycle, SubscriptionId) - --- | The Halo component state used during evaluation. -newtype HaloState props state action = HaloState - { eval :: Lifecycle props action -> HaloM props state action Aff Unit - , update :: state -> Effect Unit - , finalized :: Ref Boolean - , props :: Ref props - , state :: Ref state - , fresh :: Ref Int - , subscriptions :: Ref (Map SubscriptionId Subscription) - , forks :: Ref (Map ForkId (Fiber Unit)) - } - --- | Creates a starting `HaloState`, ready for initialization. -createInitialState - :: forall props state action - . { props :: props - , state :: state - , eval :: Lifecycle props action -> HaloM props state action Aff Unit - , update :: state -> Effect Unit - } - -> Effect (HaloState props state action) -createInitialState spec@{ eval, update } = do - finalized <- Ref.new false - fresh' <- Ref.new 0 - props <- Ref.new spec.props - state <- Ref.new spec.state - subscriptions <- Ref.new Map.empty - forks <- Ref.new Map.empty - pure $ HaloState { eval, update, finalized, props, state, fresh: fresh', subscriptions, forks } - --- | Issue a new identifier, unique to this component. -fresh :: forall props state action a. (Int -> a) -> HaloState props state action -> Effect a -fresh f (HaloState s) = Ref.modify' (\a -> { state: a + 1, value: f a }) s.fresh diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index 32b3f17..b44720f 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -1,19 +1,78 @@ -module React.Halo.Internal.Types where +module React.Halo.Internal.Types + ( Activity(..) + , ErrorContext(..) + , ForkId(..) + , Lifecycle(..) + , SubscriptionId(..) + , TaskCounts + , TaskPolicy(..) + , activityFor + , activityTotals + , emptyActivity + ) where import Prelude --- | The Halo lifecycle events. +import Data.Map (Map) +import Data.Map as Map +import Data.Maybe (Maybe(..)) + +-- | Evaluations driven by the React component lifecycle and by dispatched actions. -- | --- | - `Initialize` contains the initial props. It occurs when the component mounts, and only once per component. --- | - `Update` contains the previous and new props. It occurs when the component re-renders and the props have changes. --- | - `Action` contains the dispatched action. It occurs each time an action is dispatched to be eval'd, up until the --- | `Finalize` event. --- | - `Finalize` occurs when the component unmounts. +-- | React may activate, deactivate, and reactivate the same hook instance (notably +-- | in development StrictMode), so `Activate` is repeatable. `Update` carries the +-- | previous props; current props are available from `Halo.props`. data Lifecycle props action - = Initialize + = Activate | Update props | Action action - | Finalize + +-- | How a dispatched action is scheduled. Keyed policies coordinate actions that +-- | return the same user-defined key. `Every` is intentionally unkeyed. +data TaskPolicy key + = Every + | Restartable key + | Drop key + | Enqueue key + | KeepLatest key + +-- | The evaluation whose unexpected `Aff` failure reached the error handler. +data ErrorContext props action + = ActivationError + | UpdateError props + | ActionError action + +-- | Running and queued work counts. +type TaskCounts = + { running :: Int + , queued :: Int + } + +-- | A renderable snapshot of scheduler activity. Unkeyed `Every` work appears +-- | in the totals but not under a key. +newtype Activity key = Activity + { total :: TaskCounts + , byKey :: Map key TaskCounts + } + +derive newtype instance eqActivity :: Eq key => Eq (Activity key) + +derive newtype instance showActivity :: Show key => Show (Activity key) + +emptyActivity :: forall key. Activity key +emptyActivity = Activity + { total: { running: 0, queued: 0 } + , byKey: Map.empty + } + +activityTotals :: forall key. Activity key -> TaskCounts +activityTotals (Activity activity) = activity.total + +activityFor :: forall key. Ord key => key -> Activity key -> TaskCounts +activityFor key (Activity activity) = + case Map.lookup key activity.byKey of + Just counts -> counts + Nothing -> { running: 0, queued: 0 } newtype SubscriptionId = SubscriptionId Int @@ -21,8 +80,12 @@ derive newtype instance eqSubscriptionId :: Eq SubscriptionId derive newtype instance ordSubscriptionId :: Ord SubscriptionId +derive newtype instance showSubscriptionId :: Show SubscriptionId + newtype ForkId = ForkId Int derive newtype instance eqForkId :: Eq ForkId derive newtype instance ordForkId :: Ord ForkId + +derive newtype instance showForkId :: Show ForkId diff --git a/test/Main.purs b/test/Main.purs index 0ba98f2..5cc94a1 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -1,134 +1,16 @@ module Test.Main where import Prelude -import Control.Monad.State (modify_, put) -import Data.Maybe (Maybe(..)) + import Effect (Effect) -import Effect.Aff (Milliseconds(..), delay, launchAff_, parallel, sequential) -import Effect.Aff.Class (liftAff) -import Effect.Class (liftEffect) -import Effect.Ref as Ref -import React.Halo as Halo -import React.Halo.Internal.Eval as Eval -import React.Halo.Internal.State as State -import Test.Spec (Spec, describe, it) -import Test.Spec.Assertions (shouldEqual) +import Test.Halo.LifecycleSpec as LifecycleSpec +import Test.Halo.SchedulerSpec as SchedulerSpec +import Test.Halo.SubscriptionErrorSpec as SubscriptionErrorSpec import Test.Spec.Reporter (consoleReporter) -import Test.Spec.Runner (runSpec) +import Test.Spec.Runner.Node (runSpecAndExitProcess) main :: Effect Unit -main = - launchAff_ do - runSpec [ consoleReporter ] do - describe "purescript-react-halo" do - describe "Props" runPropTests - describe "State" runStateTests - describe "Subscriptions" runSubscriptionTests - describe "Parallelism" runParallelismTests - describe "Forking" runForkingTests - -runPropTests :: Spec Unit -runPropTests = do - describe "Update" do - it "does not fire in initialization" do - { expect } <- makeUpdateState - expect 0 - it "does not fire when props are referentially equal" do - { state, initialProps, expect } <- makeUpdateState - liftEffect $ Eval.handleUpdate state initialProps - expect 0 - it "does fire when props are not referentially equal" do - { state, expect } <- makeUpdateState - liftEffect $ Eval.handleUpdate state { value: "new object" } - expect 1 - where - makeUpdateState = - liftEffect do - count <- Ref.new 0 - let - eval = case _ of - Halo.Update _ -> liftEffect $ Ref.modify_ (add 1) count - _ -> pure unit - - initialProps = { value: "" } - - expect x = liftEffect (Ref.read count) >>= shouldEqual x - state <- State.createInitialState { props: initialProps, state: unit, eval, update: mempty } - Eval.runInitialize state - pure { state, initialProps, expect } - -runStateTests :: Spec Unit -runStateTests = do - it "correctly modifies the state" do - { modify, expect, read } <- makeState { value: "" } - modify \s -> s { value = "first" } - modify \s -> s { value = s.value <> " test" } - value <- read - value `shouldEqual` { value: "first test" } - expect 2 - it "does not modify the state when the reference has not changed" do - { modify, expect, read } <- makeState { value: "" } - modify identity - value <- read - value `shouldEqual` { value: "" } - expect 0 - where - makeState initialState = - liftEffect do - count <- Ref.new 0 - value <- Ref.new initialState - let - update state = do - Ref.write state value - Ref.modify_ (add 1) count - - read = liftEffect $ Ref.read value - - expect x = liftEffect (Ref.read count) >>= shouldEqual x - - eval = case _ of - Halo.Action f -> modify_ f - _ -> pure unit - state <- State.createInitialState { props: unit, state: initialState, eval, update } - Eval.runInitialize state - let - modify = liftEffect <<< Eval.handleAction state - pure { expect, modify, state, read } - -runSubscriptionTests :: Spec Unit -runSubscriptionTests = pure unit - -runParallelismTests :: Spec Unit -runParallelismTests = do - it "should run logic in parallel" do - state <- - liftEffect do - internalState <- Ref.new Nothing - state <- - State.createInitialState - { props: unit - , state: 0 - , update: \x -> Ref.write (Just x) internalState - , eval: - \_ -> do - c <- - sequential ado - a <- - parallel do - liftAff $ delay $ Milliseconds 1_000.0 - pure 1 - b <- - parallel do - liftAff $ delay $ Milliseconds 1_000.0 - pure 2 - in a + b - put c - } - Eval.runInitialize state - pure internalState - delay $ Milliseconds 1_100.0 - c <- liftEffect $ Ref.read state - c `shouldEqual` (Just 3) - -runForkingTests :: Spec Unit -runForkingTests = pure unit +main = runSpecAndExitProcess [ consoleReporter ] do + SchedulerSpec.spec + LifecycleSpec.spec + SubscriptionErrorSpec.spec diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs new file mode 100644 index 0000000..e6f6d1e --- /dev/null +++ b/test/Test/Halo/DocExamples.purs @@ -0,0 +1,101 @@ +module Test.Halo.DocExamples where + +import Prelude + +import Control.Monad.State (modify_) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Effect.Aff (Aff, attempt) +import Effect.Aff.Class (liftAff) +import Effect.Class.Console as Console +import Effect.Exception (message) +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) +import React.Basic.Hooks (Component) +import React.Halo as Halo + +newtype Props = Props { loadGreeting :: Aff String } + +type State = + { loading :: Boolean + , result :: Maybe (Either String String) + } + +data Action = Load + +data Task = GreetingRequest + +derive instance eqTask :: Eq Task +derive instance ordTask :: Ord Task + +loadButton :: Component Props +loadButton = Halo.component "LoadButton" + { initialState: \_ -> { loading: false, result: Nothing } + , schedule: \Load -> Halo.Restartable GreetingRequest + , eval: case _ of + Halo.Action Load -> do + modify_ _ { loading = true, result = Nothing } + Props { loadGreeting } <- Halo.props + outcome <- liftAff $ attempt loadGreeting + modify_ _ + { loading = false + , result = Just $ case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + } + _ -> pure unit + , onError: \context error -> + Console.error $ "Unexpected Halo failure in " <> showContext context <> ": " <> message error + , render: \{ state, dispatch, activity } -> + let + counts = Halo.activityFor GreetingRequest activity + in + R.div_ + [ R.button + { onClick: capture_ (dispatch Load) + , children: [ R.text if counts.running > 0 then "Restart load" else "Load" ] + } + , R.text $ case state.result of + Nothing -> if state.loading then "Loading…" else "Not loaded" + Just (Left error) -> error + Just (Right greeting) -> greeting + ] + } + +showContext :: Halo.ErrorContext Props Action -> String +showContext = case _ of + Halo.ActivationError -> "activation" + Halo.UpdateError _ -> "props update" + Halo.ActionError Load -> "Load" + +data WorkflowAction + = SearchChanged String + | SaveClicked + | Autosave String + | UploadChunk Int Int + | RecordMetric String + +data WorkflowTask + = SearchRequest + | SaveRequest + | AutosaveRequest + | Upload Int + +derive instance eqWorkflowTask :: Eq WorkflowTask +derive instance ordWorkflowTask :: Ord WorkflowTask + +workflowSchedule :: WorkflowAction -> Halo.TaskPolicy WorkflowTask +workflowSchedule = case _ of + SearchChanged _ -> Halo.Restartable SearchRequest + SaveClicked -> Halo.Drop SaveRequest + Autosave _ -> Halo.KeepLatest AutosaveRequest + UploadChunk fileId _ -> Halo.Enqueue (Upload fileId) + RecordMetric _ -> Halo.Every + +data SimpleAction = InitializeData + +simpleEval :: Halo.Lifecycle Unit SimpleAction -> Halo.HaloM Unit Unit SimpleAction Unit Unit +simpleEval = Halo.mkEval $ Halo.defaultEval + { initialize = Just InitializeData + , handleAction = \InitializeData -> pure unit + } diff --git a/test/Test/Halo/Helpers.purs b/test/Test/Halo/Helpers.purs new file mode 100644 index 0000000..339a0c5 --- /dev/null +++ b/test/Test/Halo/Helpers.purs @@ -0,0 +1,154 @@ +module Test.Halo.Helpers + ( Action(..) + , Gate + , Harness + , Key(..) + , await + , awaitCounts + , makeGate + , makeHarness + , policyOf + , release + , runAction + , shouldNotHaveStarted + , withHarness + ) where + +import Prelude + +import Control.Alt ((<|>)) +import Control.Monad.State (modify_) +import Control.Parallel (parallel, sequential) +import Data.Array as Array +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Aff (Aff, Milliseconds(..)) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.Class (liftEffect) +import Effect.Exception (message) +import Effect.AVar (AVar) +import Effect.AVar as EffectAVar +import Effect.Ref (Ref) +import Effect.Ref as Ref +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate) +import React.Halo.Internal.Types (Activity, ErrorContext(..), Lifecycle(..), TaskCounts, TaskPolicy(..), activityTotals, emptyActivity) +import Test.Spec.Assertions (fail, shouldEqual) + +data Key = Search | Save + +derive instance eqKey :: Eq Key +derive instance ordKey :: Ord Key +instance showKey :: Show Key where + show Search = "Search" + show Save = "Save" + +type Gate = + { release :: AVar Unit + , settled :: AVar Unit + , started :: AVar Unit + } + +data Action + = Work (TaskPolicy Key) Int Gate + | Boom Gate + +type Harness = + { activity :: Ref (Activity Key) + , activityChanged :: AVar Unit + , errors :: Ref (Array String) + , errorRaised :: AVar Unit + , runtime :: Runtime Unit (Array Int) Action Key + , state :: Ref (Array Int) + } + +makeGate :: Effect Gate +makeGate = do + started <- EffectAVar.empty + releaseGate <- EffectAVar.empty + settled <- EffectAVar.empty + pure { started, release: releaseGate, settled } + +policyOf :: Action -> TaskPolicy Key +policyOf = case _ of + Work policy _ _ -> policy + Boom _ -> Every + +runAction :: Lifecycle Unit Action -> HaloM Unit (Array Int) Action Key Unit +runAction = case _ of + Action (Work _ value gate) -> do + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release + modify_ (flip Array.snoc value) + Action (Boom gate) -> + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + (Aff.throwError (Aff.error "boom")) + Activate -> pure unit + Update _ -> pure unit + +makeHarness :: Aff Harness +makeHarness = liftEffect do + activity <- Ref.new emptyActivity + activityChanged <- EffectAVar.empty + errors <- Ref.new [] + errorRaised <- EffectAVar.empty + state <- Ref.new [] + runtime <- createRuntime + { activityUpdate: \next -> do + Ref.write next activity + void $ EffectAVar.tryPut unit activityChanged + , initialProps: unit + , initialState: [] + , spec: + { eval: runAction + , onError: \context error -> do + Ref.modify_ (\current -> Array.snoc current (contextName context <> ": " <> message error)) errors + void $ EffectAVar.tryPut unit errorRaised + , schedule: policyOf + } + , stateUpdate: flip Ref.write state + } + activate runtime + pure { activity, activityChanged, errors, errorRaised, runtime, state } + +withHarness :: (Harness -> Aff Unit) -> Aff Unit +withHarness test = do + harness <- makeHarness + Aff.finally (liftEffect $ deactivate harness.runtime) (test harness) + +await :: forall a. String -> AVar a -> Aff a +await label value = + sequential $ + parallel (AVar.take value) + <|> parallel (Aff.delay (Milliseconds 2_000.0) *> Aff.throwError (Aff.error ("Timed out waiting for " <> label))) + +release :: Gate -> Aff Unit +release = AVar.put unit <<< _.release + +shouldNotHaveStarted :: Gate -> Aff Unit +shouldNotHaveStarted gate = do + started <- AVar.tryTake gate.started + started `shouldEqual` Nothing + +awaitCounts :: Harness -> TaskCounts -> Aff Unit +awaitCounts harness expected = go 20 + where + go remaining = do + actual <- activityTotals <$> liftEffect (Ref.read harness.activity) + if actual == expected then pure unit + else if remaining <= 0 then + fail $ "Expected activity " <> show expected <> " but got " <> show actual + else do + void $ await "activity update" harness.activityChanged + go (remaining - 1) + +contextName :: ErrorContext Unit Action -> String +contextName = case _ of + ActivationError -> "activation" + UpdateError _ -> "update" + ActionError _ -> "action" diff --git a/test/Test/Halo/LifecycleSpec.purs b/test/Test/Halo/LifecycleSpec.purs new file mode 100644 index 0000000..7c52963 --- /dev/null +++ b/test/Test/Halo/LifecycleSpec.purs @@ -0,0 +1,238 @@ +module Test.Halo.LifecycleSpec (spec) where + +import Prelude + +import Control.Monad.State (modify_) +import Effect (Effect) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.AVar (AVar) +import Effect.AVar as EffectAVar +import Effect.Class (liftEffect) +import Effect.Ref as Ref +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, runForTest, syncSpec, updateProps) +import React.Halo.Internal.Types (ErrorContext(..), Lifecycle(..), TaskPolicy(..)) +import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, policyOf, release, shouldNotHaveStarted, withHarness) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec :: Spec Unit +spec = describe "scope lifecycle" do + it "cancels running and queued work, clears activity, and fences commits on deactivation" $ withHarness \harness -> do + running <- liftEffect makeGate + queued <- liftEffect makeGate + ignored <- liftEffect makeGate + + liftEffect do + dispatch harness.runtime (Work (Enqueue Save) 1 running) + dispatch harness.runtime (Work (Enqueue Save) 2 queued) + void $ await "running action start before deactivation" running.started + awaitCounts harness { running: 1, queued: 1 } + + liftEffect $ deactivate harness.runtime + void $ await "running action cancellation on deactivation" running.settled + awaitCounts harness { running: 0, queued: 0 } + shouldNotHaveStarted queued + + liftEffect $ dispatch harness.runtime (Work Every 3 ignored) + shouldNotHaveStarted ignored + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [] + + reactivated <- liftEffect makeGate + liftEffect do + activate harness.runtime + dispatch harness.runtime (Work Every 4 reactivated) + void $ await "action start after reactivation" reactivated.started + release reactivated + void $ await "action completion after reactivation" reactivated.settled + awaitCounts harness { running: 0, queued: 0 } + reactivatedState <- liftEffect $ Ref.read harness.state + reactivatedState `shouldEqual` [ 4 ] + + it "models StrictMode setup-cleanup-setup with a fresh usable active scope" do + activation <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: 0 + , spec: + { eval: replayEval activation + , onError: \_ _ -> pure unit + , schedule: \_ -> Every + } + , stateUpdate: flip Ref.write state + } + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect $ activate runtime + void $ await "first activation" activation + first <- liftEffect $ Ref.read state + first `shouldEqual` 1 + + liftEffect do + deactivate runtime + activate runtime + void $ await "StrictMode replay activation" activation + + pulse <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (Pulse pulse) + void $ await "action after StrictMode replay" pulse + second <- liftEffect $ Ref.read state + second `shouldEqual` 12 + + it "owns and cancels prop-update evaluations" do + gate <- liftEffect makeGate + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ + ( createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: 0 + , initialState: 0 + , spec: + { eval: case _ of + Update _ -> do + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release + modify_ (_ + 1) + _ -> pure unit + , onError: \_ _ -> pure unit + , schedule: \_ -> Every + } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Int Int Unit Unit) + ) + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + updateProps runtime 1 + void $ await "props update evaluation start" gate.started + liftEffect $ deactivate runtime + void $ await "props update evaluation cancellation" gate.settled + value <- liftEffect $ Ref.read state + value `shouldEqual` 0 + + it "cancels structured child tasks with their owning evaluation" $ withHarness \harness -> do + childStarted <- liftEffect EffectAVar.empty + childSettled <- liftEffect EffectAVar.empty + parentRelease <- liftEffect EffectAVar.empty + + liftEffect $ runForTest harness.runtime ActivationError do + void $ fork do + liftAff $ Aff.finally + (void $ AVar.tryPut unit childSettled) + do + AVar.put unit childStarted + void $ AVar.take parentRelease + liftAff $ void $ AVar.take parentRelease + + void $ await "structured child start" childStarted + liftEffect $ deactivate harness.runtime + void $ await "structured child cancellation" childSettled + + it "commit-fences structured children when a Restartable parent is replaced" do + firstParent <- liftEffect makeGate + firstChild <- liftEffect makeGate + replacement <- liftEffect makeGate + replacementDone <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: 0 + , spec: + { eval: childEval + , onError: \_ _ -> pure unit + , schedule: \_ -> Restartable unit + } + , stateUpdate: flip Ref.write state + } + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (ParentWithChild firstParent firstChild) + void $ await "Restartable parent start" firstParent.started + void $ await "Restartable child start" firstChild.started + + liftEffect $ dispatch runtime (Replacement replacement replacementDone) + void $ await "replaced parent cancellation" firstParent.settled + void $ await "replaced child cancellation" firstChild.settled + void $ await "replacement parent start" replacement.started + release replacement + void $ await "replacement parent state commit" replacementDone + + value <- liftEffect $ Ref.read state + value `shouldEqual` 10 + + it "uses the latest evaluator and handlers after the hook spec changes" $ withHarness \harness -> do + gate <- liftEffect makeGate + liftEffect do + syncSpec harness.runtime + { activityUpdate: \next -> do + Ref.write next harness.activity + void $ EffectAVar.tryPut unit harness.activityChanged + , spec: + { eval: case _ of + Action (Work _ value workGate) -> do + liftAff do + AVar.put unit workGate.started + void $ AVar.take workGate.release + modify_ (flip append [ value * 10 ]) + _ -> pure unit + , onError: \_ _ -> pure unit + , schedule: policyOf + } + , stateUpdate: flip Ref.write harness.state + } + dispatch harness.runtime (Work Every 2 gate) + + void $ await "action using replacement evaluator" gate.started + release gate + awaitCounts harness { running: 0, queued: 0 } + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 20 ] + +data ChildAction + = ParentWithChild Gate Gate + | Replacement Gate (AVar Unit) + +childEval :: Lifecycle Unit ChildAction -> HaloM Unit Int ChildAction Unit Unit +childEval = case _ of + Activate -> pure unit + Update _ -> pure unit + Action (ParentWithChild parent child) -> do + void $ fork do + runGate child + modify_ (_ + 100) + runGate parent + modify_ (_ + 1) + Action (Replacement gate completed) -> do + runGate gate + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit completed + +runGate :: forall props state action key. Gate -> HaloM props state action key Unit +runGate gate = liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release + +data ReplayAction = Pulse (AVar Unit) + +replayEval :: AVar Unit -> Lifecycle Unit ReplayAction -> HaloM Unit Int ReplayAction Unit Unit +replayEval activation = case _ of + Activate -> do + modify_ (_ + 1) + liftAff $ void $ AVar.tryPut unit activation + Action (Pulse completed) -> do + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit completed + Update _ -> pure unit diff --git a/test/Test/Halo/SchedulerSpec.purs b/test/Test/Halo/SchedulerSpec.purs new file mode 100644 index 0000000..febfe57 --- /dev/null +++ b/test/Test/Halo/SchedulerSpec.purs @@ -0,0 +1,145 @@ +module Test.Halo.SchedulerSpec (spec) where + +import Prelude + +import Data.Array as Array +import Effect.Class (liftEffect) +import Effect.Ref as Ref +import React.Halo.Internal.Runtime (dispatch) +import React.Halo.Internal.Types (TaskPolicy(..), activityFor) +import Test.Halo.Helpers (Action(..), Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec :: Spec Unit +spec = describe "action scheduling" do + it "Every runs every action concurrently" $ withHarness \harness -> do + first <- liftEffect makeGate + second <- liftEffect makeGate + + liftEffect do + dispatch harness.runtime (Work Every 1 first) + dispatch harness.runtime (Work Every 2 second) + + void $ await "first Every action start" first.started + void $ await "second Every action start" second.started + awaitCounts harness { running: 2, queued: 0 } + + release second + release first + void $ await "first Every action completion" first.settled + void $ await "second Every action completion" second.settled + awaitCounts harness { running: 0, queued: 0 } + + state <- liftEffect $ Ref.read harness.state + Array.sort state `shouldEqual` [ 1, 2 ] + + it "Restartable cancels and commit-fences the previous keyed action" $ withHarness \harness -> do + stale <- liftEffect makeGate + current <- liftEffect makeGate + + liftEffect $ dispatch harness.runtime (Work (Restartable Search) 1 stale) + void $ await "stale Restartable action start" stale.started + liftEffect $ dispatch harness.runtime (Work (Restartable Search) 2 current) + + void $ await "stale Restartable action cancellation" stale.settled + void $ await "replacement Restartable action start" current.started + awaitCounts harness { running: 1, queued: 0 } + + release current + void $ await "replacement Restartable action completion" current.settled + awaitCounts harness { running: 0, queued: 0 } + + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 2 ] + + it "Drop ignores new work while the key is running" $ withHarness \harness -> do + running <- liftEffect makeGate + dropped <- liftEffect makeGate + + liftEffect $ dispatch harness.runtime (Work (Drop Save) 1 running) + void $ await "Drop action start" running.started + liftEffect $ dispatch harness.runtime (Work (Drop Save) 2 dropped) + awaitCounts harness { running: 1, queued: 0 } + shouldNotHaveStarted dropped + + release running + void $ await "Drop action completion" running.settled + awaitCounts harness { running: 0, queued: 0 } + shouldNotHaveStarted dropped + + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 1 ] + + it "Enqueue runs all keyed actions FIFO, one at a time" $ withHarness \harness -> do + first <- liftEffect makeGate + second <- liftEffect makeGate + third <- liftEffect makeGate + + liftEffect do + dispatch harness.runtime (Work (Enqueue Save) 1 first) + dispatch harness.runtime (Work (Enqueue Save) 2 second) + dispatch harness.runtime (Work (Enqueue Save) 3 third) + + void $ await "first Enqueue action start" first.started + awaitCounts harness { running: 1, queued: 2 } + shouldNotHaveStarted second + shouldNotHaveStarted third + + release first + void $ await "second Enqueue action start" second.started + awaitCounts harness { running: 1, queued: 1 } + release second + void $ await "third Enqueue action start" third.started + awaitCounts harness { running: 1, queued: 0 } + release third + void $ await "third Enqueue action completion" third.settled + awaitCounts harness { running: 0, queued: 0 } + + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 1, 2, 3 ] + + it "KeepLatest finishes current work and retains only the newest queued action" $ withHarness \harness -> do + first <- liftEffect makeGate + discarded <- liftEffect makeGate + latest <- liftEffect makeGate + + liftEffect do + dispatch harness.runtime (Work (KeepLatest Search) 1 first) + dispatch harness.runtime (Work (KeepLatest Search) 2 discarded) + dispatch harness.runtime (Work (KeepLatest Search) 3 latest) + + void $ await "current KeepLatest action start" first.started + awaitCounts harness { running: 1, queued: 1 } + shouldNotHaveStarted discarded + shouldNotHaveStarted latest + + release first + void $ await "latest KeepLatest action start" latest.started + shouldNotHaveStarted discarded + release latest + void $ await "latest KeepLatest action completion" latest.settled + awaitCounts harness { running: 0, queued: 0 } + + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 1, 3 ] + + it "reports keyed running and queued activity for rendering" $ withHarness \harness -> do + running <- liftEffect makeGate + queued <- liftEffect makeGate + + liftEffect do + dispatch harness.runtime (Work (Enqueue Search) 1 running) + dispatch harness.runtime (Work (Enqueue Search) 2 queued) + void $ await "keyed activity action start" running.started + awaitCounts harness { running: 1, queued: 1 } + + activity <- liftEffect $ Ref.read harness.activity + activityFor Search activity `shouldEqual` { running: 1, queued: 1 } + activityFor Save activity `shouldEqual` { running: 0, queued: 0 } + + release running + void $ await "queued keyed activity action start" queued.started + release queued + void $ await "queued keyed activity action completion" queued.settled + awaitCounts harness { running: 0, queued: 0 } diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs new file mode 100644 index 0000000..40d4439 --- /dev/null +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -0,0 +1,128 @@ +module Test.Halo.SubscriptionErrorSpec (spec) where + +import Prelude + +import Control.Monad.State (get, put) +import Data.Foldable (traverse_) +import Data.Maybe (Maybe(..)) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.AVar (AVar) +import Effect.AVar as EffectAVar +import Effect.Class (liftEffect) +import Effect.Ref as Ref +import Halogen.Subscription (Emitter) +import Halogen.Subscription as HS +import React.Halo.Internal.Runtime (HaloM, activate, createRuntime, deactivate, dispatch, subscribe, unsubscribe) +import React.Halo.Internal.Types (Lifecycle(..), SubscriptionId, TaskPolicy(..), activityTotals, emptyActivity) +import Test.Halo.Helpers (Action(..), await, awaitCounts, makeGate, withHarness) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec :: Spec Unit +spec = describe "subscriptions and errors" do + it "removes a manually unsubscribed resource from component tracking" do + cleanupCount <- liftEffect $ Ref.new 0 + callback <- liftEffect $ Ref.new Nothing + started <- liftEffect EffectAVar.empty + stopped <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new Nothing + activity <- liftEffect $ Ref.new emptyActivity + let + emitter = HS.makeEmitter \receive -> do + Ref.write (Just receive) callback + pure $ Ref.modify_ (_ + 1) cleanupCount + + runtime <- liftEffect $ createRuntime + { activityUpdate: flip Ref.write activity + , initialProps: unit + , initialState: Nothing + , spec: + { eval: subscriptionEval + , onError: \_ _ -> pure unit + , schedule: \_ -> Every + } + , stateUpdate: flip Ref.write state + } + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (Start emitter started) + void $ await "subscription setup" started + liftEffect $ dispatch runtime (Stop stopped) + void $ await "manual unsubscribe" stopped + + afterManual <- liftEffect $ Ref.read cleanupCount + afterManual `shouldEqual` 1 + liftEffect do + deactivate runtime + activate runtime + afterDeactivation <- liftEffect $ Ref.read cleanupCount + afterDeactivation `shouldEqual` 1 + + -- Even if a broken source invokes its retained callback after cleanup, + -- that callback is bound to the old scope and cannot target reactivation. + retained <- liftEffect $ Ref.read callback + liftEffect $ traverse_ (_ $ Ping) retained + counts <- activityTotals <$> liftEffect (Ref.read activity) + counts `shouldEqual` { running: 0, queued: 0 } + + it "unsubscribes tracked resources on deactivation" do + cleanupCount <- liftEffect $ Ref.new 0 + started <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new Nothing + let emitter = HS.makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleanupCount + + runtime <- liftEffect $ createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: Nothing + , spec: + { eval: subscriptionEval + , onError: \_ _ -> pure unit + , schedule: \_ -> Every + } + , stateUpdate: flip Ref.write state + } + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (Start emitter started) + void $ await "tracked subscription setup" started + liftEffect $ deactivate runtime + cleaned <- liftEffect $ Ref.read cleanupCount + cleaned `shouldEqual` 1 + + it "routes unexpected action failures with action context" $ withHarness \harness -> do + gate <- liftEffect makeGate + liftEffect $ dispatch harness.runtime (Boom gate) + void $ await "spec-level error handler" harness.errorRaised + awaitCounts harness { running: 0, queued: 0 } + + errors <- liftEffect $ Ref.read harness.errors + errors `shouldEqual` [ "action: boom" ] + +data SubscriptionAction + = Start (Emitter SubscriptionAction) (AVar Unit) + | Stop (AVar Unit) + | Ping + +subscriptionEval + :: Lifecycle Unit SubscriptionAction + -> HaloM Unit (Maybe SubscriptionId) SubscriptionAction Unit Unit +subscriptionEval = case _ of + Activate -> pure unit + Update _ -> pure unit + Action (Start emitter completed) -> do + sid <- subscribe emitter + put (Just sid) + liftAff $ void $ AVar.tryPut unit completed + Action (Stop completed) -> do + sid <- get + traverse_ unsubscribe sid + put Nothing + liftAff $ void $ AVar.tryPut unit completed + Action Ping -> pure unit From 8f990906500c3300cc1c753d712d890620b6266b Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 12:00:18 +0900 Subject: [PATCH 02/16] Harden cleanup and simplify subscriptions --- README.md | 33 +++++++++--- spago.lock | 19 +------ spago.yaml | 2 +- src/React/Halo.purs | 3 +- src/React/Halo/Internal/Control.purs | 52 ------------------- src/React/Halo/Internal/Runtime.purs | 26 +++++++--- src/React/Halo/Internal/Types.purs | 1 + src/React/Halo/Subscription.purs | 31 ++++++++++++ test/Test/Halo/DocExamples.purs | 4 ++ test/Test/Halo/Helpers.purs | 1 + test/Test/Halo/SubscriptionErrorSpec.purs | 61 ++++++++++++++++++++--- 11 files changed, 141 insertions(+), 92 deletions(-) delete mode 100644 src/React/Halo/Internal/Control.purs create mode 100644 src/React/Halo/Subscription.purs diff --git a/README.md b/README.md index dbb8047..04b4b46 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,27 @@ Use Halo when a component has event-driven workflows that are awkward to express ## Install -Halo v4 targets PureScript 0.15.16, Spago 1.0.4, and the Registry package set 80.8.0 used by this repository. +Halo v4 targets PureScript 0.15.16 and Spago 1.0.4. It is not published yet: the Registry still resolves `react-halo` to v3. To try v4 from a sibling checkout, add Halo as a local package and include `react-basic-dom` for the quick-start renderer: + +```yaml +package: + dependencies: + - react-basic-dom + - react-halo + +workspace: + extraPackages: + react-halo: + path: ../purescript-react-halo +``` + +After v4 is published, install both packages with: ```console -spago install react-halo +spago install react-halo react-basic-dom ``` -Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo does not publish an npm runtime entry point. +Halo itself does not require `react-basic-dom`; only the example renderer does. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo does not publish an npm runtime entry point. ## Quick start: a restartable request @@ -177,9 +191,13 @@ Cancellation cannot undo an HTTP request already sent, a log already written, or ## Subscriptions -Halo uses `Emitter` from `halogen-subscriptions`: +Halo has a small emitter type rather than depending on Halogen. Registration receives an action callback and must return that receiver's cleanup effect: ```purescript +eventEmitter = Halo.makeEmitter \emit -> do + listener <- source.listen emit + pure (source.remove listener) + Halo.Action StartListening -> do subscriptionId <- Halo.subscribe eventEmitter modify_ _ { subscriptionId = Just subscriptionId } @@ -190,7 +208,7 @@ Halo.Action StopListening -> do modify_ _ { subscriptionId = Nothing } ``` -Manual `unsubscribe` removes the subscription from Halo's tracking. Any subscription still tracked at deactivation is unsubscribed automatically. New subscriptions from stale or inactive evaluations are rejected, and callbacks retained by a misbehaving source remain bound to their original scope rather than targeting a later reactivation. +Manual `unsubscribe` removes the cleanup from Halo's tracking before running it. Any cleanup still tracked at deactivation is run automatically. One cleanup failure is reported through `onError` only after Halo has attempted every subscription cleanup and requested cancellation of all other scope-owned work. New subscriptions from stale or inactive evaluations are rejected, and callbacks retained by a misbehaving source remain bound to their original scope rather than targeting a later reactivation. An `Emitter` is broadcast-style: every subscriber receives every emitted value. It is not a consuming work queue and provides no backpressure. Each event delivered to Halo is dispatched once and then follows its action policy. Halo v4 intentionally does not expose a coroutine, process, or saga API; task scheduling is the focused concurrency boundary. @@ -202,7 +220,7 @@ Every spec must provide: onError :: Halo.ErrorContext props action -> Error -> Effect Unit ``` -The context is `ActivationError`, `UpdateError previousProps`, or `ActionError action`. Expected domain failures belong in the action/state model, usually by catching `Aff` errors inside `eval`. Unexpected uncaught errors go to `onError`. Cancellation caused by replacement or deactivation is suppressed rather than reported as an application failure. +The context is `ActivationError`, `DeactivationError`, `UpdateError previousProps`, or `ActionError action`. `DeactivationError` reports a subscription cleanup that threw; Halo continues cleaning the rest of the scope before calling `onError`. Expected domain failures belong in the action/state model, usually by catching `Aff` errors inside `eval`. Unexpected uncaught errors go to `onError`. Cancellation caused by replacement or deactivation is suppressed rather than reported as an application failure. ## Component helper or hook @@ -251,13 +269,14 @@ Version 4 intentionally breaks the evaluator API to make cancellation and owners - Change `HaloM props state action m` to `HaloM props state action key`. Halo now runs directly on `Aff`; remove `hoist`, `HaloAp`, and the custom base monad parameter. - Add an application task-key type with `Eq` and `Ord`, then add `schedule :: action -> TaskPolicy key`. -- Add `onError :: ErrorContext props action -> Error -> Effect Unit`. +- Add `onError :: ErrorContext props action -> Error -> Effect Unit`, including the new `DeactivationError` context for subscription cleanup failures. - Replace `Initialize` with `Activate`. `Activate` is repeatable. - Remove `Finalize` handlers. Use scoped cancellation, subscriptions, and `Aff` finalizers instead. - Keep `Update previousProps`, and read current props with `Halo.props`. - Replace the `useHalo` tuple with the record fields `state`, `dispatch`, and `activity`. - In `component` renderers, rename `send` to `dispatch` and accept `activity` when needed. - Revisit `fork`: v4 children are structured under the evaluation that created them, not detached until component unmount. +- Replace `Halogen.Subscription.Emitter` values with `Halo.makeEmitter`; the registration function has the same callback-and-cleanup shape but no Halogen dependency. - Remove assumptions that action effects run without coordination. Choose `Every` explicitly for v3-like concurrent dispatch. ## Development diff --git a/spago.lock b/spago.lock index 9d0570a..a1f2e38 100644 --- a/spago.lock +++ b/spago.lock @@ -10,8 +10,8 @@ "avar", "effect", "either", + "exceptions", "foldable-traversable", - "halogen-subscriptions", "maybe", "newtype", "ordered-collections", @@ -1119,23 +1119,6 @@ "unfoldable" ] }, - "halogen-subscriptions": { - "type": "registry", - "version": "2.0.0", - "integrity": "sha256-1eBtVZENgGtKuOY9H0iuYD3dO1CSqmOIyhYe4OhypOU=", - "dependencies": [ - "arrays", - "contravariant", - "control", - "effect", - "foldable-traversable", - "maybe", - "prelude", - "refs", - "safe-coerce", - "unsafe-reference" - ] - }, "identity": { "type": "registry", "version": "6.0.0", diff --git a/spago.yaml b/spago.yaml index 65dce67..7cef87e 100644 --- a/spago.yaml +++ b/spago.yaml @@ -12,8 +12,8 @@ package: - avar - effect - either + - exceptions - foldable-traversable - - halogen-subscriptions - maybe - newtype - ordered-collections diff --git a/src/React/Halo.purs b/src/React/Halo.purs index 5ecdedd..824a9c1 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -4,6 +4,7 @@ module React.Halo import React.Halo.Component (ComponentSpec, component) as Exports import React.Halo.Hook (HaloHook, HookSpec, UseHalo(..), useHalo) as Exports -import React.Halo.Internal.Control (HaloM, fork, kill, props, subscribe, subscribe', unsubscribe) as Exports import React.Halo.Internal.Eval (EvalSpec, defaultEval, mkEval) as Exports +import React.Halo.Internal.Runtime (HaloM, fork, kill, props, subscribe, subscribe', unsubscribe) as Exports import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, Lifecycle(..), SubscriptionId, TaskCounts, TaskPolicy(..), activityFor, activityTotals, emptyActivity) as Exports +import React.Halo.Subscription (Emitter, makeEmitter) as Exports diff --git a/src/React/Halo/Internal/Control.purs b/src/React/Halo/Internal/Control.purs deleted file mode 100644 index a11f24c..0000000 --- a/src/React/Halo/Internal/Control.purs +++ /dev/null @@ -1,52 +0,0 @@ -module React.Halo.Internal.Control - ( HaloM - , fork - , kill - , props - , subscribe - , subscribe' - , unsubscribe - ) where - -import Prelude - -import Halogen.Subscription (Emitter) -import React.Halo.Internal.Runtime as Runtime -import React.Halo.Internal.Types (ForkId, SubscriptionId) - -type HaloM props state action key = Runtime.HaloM props state action key - -props :: forall props state action key. HaloM props state action key props -props = Runtime.props - -subscribe - :: forall props state action key - . Ord key - => Emitter action - -> HaloM props state action key SubscriptionId -subscribe = Runtime.subscribe - -subscribe' - :: forall props state action key - . Ord key - => (SubscriptionId -> Emitter action) - -> HaloM props state action key SubscriptionId -subscribe' = Runtime.subscribe' - -unsubscribe - :: forall props state action key - . SubscriptionId - -> HaloM props state action key Unit -unsubscribe = Runtime.unsubscribe - -fork - :: forall props state action key - . HaloM props state action key Unit - -> HaloM props state action key ForkId -fork = Runtime.fork - -kill - :: forall props state action key - . ForkId - -> HaloM props state action key Unit -kill = Runtime.kill diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index 74c9ea3..9342e8e 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -36,11 +36,12 @@ import Effect.Aff.AVar as AVar import Effect.Aff.Class (class MonadAff, liftAff) import Effect.AVar as EffectAVar import Effect.Class (class MonadEffect, liftEffect) +import Effect.Exception as Exception import Effect.Ref (Ref) import Effect.Ref as Ref -import Halogen.Subscription (Emitter, Subscription) -import Halogen.Subscription as HS import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), Lifecycle(..), SubscriptionId(..), TaskPolicy(..), emptyActivity) +import React.Halo.Subscription (Emitter) +import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) -- | The direct Halo evaluator. Its environment is intentionally private so a @@ -78,7 +79,7 @@ newtype Scope props state action key = Scope , every :: Ref (Map Int (Root props state action key)) , generation :: Int , roots :: Ref (Map Int (Root props state action key)) - , subscriptions :: Ref (Map SubscriptionId Subscription) + , subscriptions :: Ref (Map SubscriptionId (Effect Unit)) , tasks :: Ref (Map key (TaskSlot props state action key)) } @@ -205,11 +206,22 @@ deactivate runtime@(Runtime state) = do subscriptions <- takeRef current.subscriptions Map.empty publishActivity runtime emptyActivity - traverse_ HS.unsubscribe (Map.values subscriptions) + cleanupResults <- traverse Exception.try (Map.values subscriptions) traverse_ cancelRoot (Map.values roots) traverse_ cancelRoot (Map.values every) traverse_ (traverse_ cancelRoot <<< Map.values <<< _.running) (Map.values tasks) + -- A faulty external cleanup must not prevent the rest of the scope from + -- being cancelled. Report teardown failures only after every owned + -- resource has received its cleanup request. + spec <- Ref.read state.spec + traverse_ + ( case _ of + Left error -> spec.onError DeactivationError error + Right _ -> pure unit + ) + cleanupResults + updateProps :: forall props state action key . Ord key @@ -276,8 +288,8 @@ subscribe' makeEmitter = HaloM do current <- isCurrent execution when current do let Scope scope = execution.scope - subscription <- HS.subscribe (makeEmitter sid) (dispatchToScope execution.runtime execution.scope) - Ref.modify_ (Map.insert sid subscription) scope.subscriptions + cleanup <- Subscription.runEmitter (makeEmitter sid) (dispatchToScope execution.runtime execution.scope) + Ref.modify_ (Map.insert sid cleanup) scope.subscriptions pure sid unsubscribe @@ -297,7 +309,7 @@ unsubscribe sid = HaloM do } ) scope.subscriptions - traverse_ HS.unsubscribe subscription + traverse_ identity subscription fork :: forall props state action key diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index b44720f..cfd8055 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -39,6 +39,7 @@ data TaskPolicy key -- | The evaluation whose unexpected `Aff` failure reached the error handler. data ErrorContext props action = ActivationError + | DeactivationError | UpdateError props | ActionError action diff --git a/src/React/Halo/Subscription.purs b/src/React/Halo/Subscription.purs new file mode 100644 index 0000000..d27b379 --- /dev/null +++ b/src/React/Halo/Subscription.purs @@ -0,0 +1,31 @@ +module React.Halo.Subscription + ( Emitter + , makeEmitter + , runEmitter + ) where + +import Prelude (Unit) + +import Effect (Effect) + +-- | A source that broadcasts actions to each registered receiver. +-- | +-- | Registration returns the cleanup effect for that receiver. Halo runs the +-- | cleanup when the subscription is removed or its activation scope ends. +newtype Emitter action = Emitter + ((action -> Effect Unit) -> Effect (Effect Unit)) + +-- | Create an emitter from registration logic. +makeEmitter + :: forall action + . ((action -> Effect Unit) -> Effect (Effect Unit)) + -> Emitter action +makeEmitter = Emitter + +-- | Register a receiver and obtain its cleanup effect. +runEmitter + :: forall action + . Emitter action + -> (action -> Effect Unit) + -> Effect (Effect Unit) +runEmitter (Emitter register) = register diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index e6f6d1e..fb73b8d 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -65,6 +65,7 @@ loadButton = Halo.component "LoadButton" showContext :: Halo.ErrorContext Props Action -> String showContext = case _ of Halo.ActivationError -> "activation" + Halo.DeactivationError -> "deactivation" Halo.UpdateError _ -> "props update" Halo.ActionError Load -> "Load" @@ -94,6 +95,9 @@ workflowSchedule = case _ of data SimpleAction = InitializeData +simpleEmitter :: Halo.Emitter SimpleAction +simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) + simpleEval :: Halo.Lifecycle Unit SimpleAction -> Halo.HaloM Unit Unit SimpleAction Unit Unit simpleEval = Halo.mkEval $ Halo.defaultEval { initialize = Just InitializeData diff --git a/test/Test/Halo/Helpers.purs b/test/Test/Halo/Helpers.purs index 339a0c5..f029929 100644 --- a/test/Test/Halo/Helpers.purs +++ b/test/Test/Halo/Helpers.purs @@ -150,5 +150,6 @@ awaitCounts harness expected = go 20 contextName :: ErrorContext Unit Action -> String contextName = case _ of ActivationError -> "activation" + DeactivationError -> "deactivation" UpdateError _ -> "update" ActionError _ -> "action" diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index 40d4439..1e2f016 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -11,12 +11,12 @@ import Effect.Aff.Class (liftAff) import Effect.AVar (AVar) import Effect.AVar as EffectAVar import Effect.Class (liftEffect) +import Effect.Exception as Exception import Effect.Ref as Ref -import Halogen.Subscription (Emitter) -import Halogen.Subscription as HS import React.Halo.Internal.Runtime (HaloM, activate, createRuntime, deactivate, dispatch, subscribe, unsubscribe) -import React.Halo.Internal.Types (Lifecycle(..), SubscriptionId, TaskPolicy(..), activityTotals, emptyActivity) -import Test.Halo.Helpers (Action(..), await, awaitCounts, makeGate, withHarness) +import React.Halo.Internal.Types (ErrorContext(..), Lifecycle(..), SubscriptionId, TaskPolicy(..), activityTotals, emptyActivity) +import React.Halo.Subscription (Emitter, makeEmitter) +import Test.Halo.Helpers (Action(..), Gate, await, awaitCounts, makeGate, withHarness) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) @@ -30,7 +30,7 @@ spec = describe "subscriptions and errors" do state <- liftEffect $ Ref.new Nothing activity <- liftEffect $ Ref.new emptyActivity let - emitter = HS.makeEmitter \receive -> do + emitter = makeEmitter \receive -> do Ref.write (Just receive) callback pure $ Ref.modify_ (_ + 1) cleanupCount @@ -73,7 +73,7 @@ spec = describe "subscriptions and errors" do cleanupCount <- liftEffect $ Ref.new 0 started <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new Nothing - let emitter = HS.makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleanupCount + let emitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleanupCount runtime <- liftEffect $ createRuntime { activityUpdate: \_ -> pure unit @@ -96,6 +96,48 @@ spec = describe "subscriptions and errors" do cleaned <- liftEffect $ Ref.read cleanupCount cleaned `shouldEqual` 1 + it "continues deactivation when a subscription cleanup throws" do + cleaned <- liftEffect $ Ref.new 0 + cleanupErrors <- liftEffect $ Ref.new [] + badStarted <- liftEffect EffectAVar.empty + goodStarted <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new Nothing + gate <- liftEffect makeGate + let + badEmitter = makeEmitter \_ -> pure $ Exception.throw "cleanup failed" + goodEmitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleaned + + runtime <- liftEffect $ createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: Nothing + , spec: + { eval: subscriptionEval + , onError: \context error -> case context of + DeactivationError -> Ref.modify_ (_ <> [ Exception.message error ]) cleanupErrors + _ -> Ref.modify_ (_ <> [ "wrong error context" ]) cleanupErrors + , schedule: \_ -> Every + } + , stateUpdate: flip Ref.write state + } + + liftEffect do + activate runtime + dispatch runtime (Start badEmitter badStarted) + void $ await "failing subscription setup" badStarted + liftEffect $ dispatch runtime (Start goodEmitter goodStarted) + void $ await "successful subscription setup" goodStarted + liftEffect $ dispatch runtime (Block gate) + void $ await "running action" gate.started + + liftEffect $ deactivate runtime + void $ await "running action cancellation" gate.settled + + cleanupCount <- liftEffect $ Ref.read cleaned + cleanupCount `shouldEqual` 1 + errors <- liftEffect $ Ref.read cleanupErrors + errors `shouldEqual` [ "cleanup failed" ] + it "routes unexpected action failures with action context" $ withHarness \harness -> do gate <- liftEffect makeGate liftEffect $ dispatch harness.runtime (Boom gate) @@ -108,6 +150,7 @@ spec = describe "subscriptions and errors" do data SubscriptionAction = Start (Emitter SubscriptionAction) (AVar Unit) | Stop (AVar Unit) + | Block Gate | Ping subscriptionEval @@ -125,4 +168,10 @@ subscriptionEval = case _ of traverse_ unsubscribe sid put Nothing liftAff $ void $ AVar.tryPut unit completed + Action (Block gate) -> + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release Action Ping -> pure unit From 643171ad0a19d7ee28c795200aeff808d55fc92e Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 12:50:27 +0900 Subject: [PATCH 03/16] Make Halo tasks explicit --- README.md | 247 +++--------------- docs/guide.md | 304 ++++++++++++++++++++++ docs/migration-v4.md | 168 ++++++++++++ docs/reference.md | 212 +++++++++++++++ src/React/Halo.purs | 6 +- src/React/Halo/Component.purs | 15 +- src/React/Halo/Handlers.purs | 25 ++ src/React/Halo/Hook.purs | 27 +- src/React/Halo/Internal/Eval.purs | 38 --- src/React/Halo/Internal/Runtime.purs | 158 ++++++----- src/React/Halo/Internal/Types.purs | 39 +-- src/React/Halo/Subscription.purs | 5 +- test/Main.purs | 4 +- test/Test/Halo/DocExamples.purs | 56 ++-- test/Test/Halo/Helpers.purs | 100 ++++--- test/Test/Halo/LifecycleSpec.purs | 238 ----------------- test/Test/Halo/SchedulerSpec.purs | 136 +++++++--- test/Test/Halo/ScopeHandlerSpec.purs | 277 ++++++++++++++++++++ test/Test/Halo/SubscriptionErrorSpec.purs | 140 +++++----- 19 files changed, 1445 insertions(+), 750 deletions(-) create mode 100644 docs/guide.md create mode 100644 docs/migration-v4.md create mode 100644 docs/reference.md create mode 100644 src/React/Halo/Handlers.purs delete mode 100644 src/React/Halo/Internal/Eval.purs delete mode 100644 test/Test/Halo/LifecycleSpec.purs create mode 100644 test/Test/Halo/ScopeHandlerSpec.purs diff --git a/README.md b/README.md index 04b4b46..90150a6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,27 @@ # React Halo -Halo gives PureScript React components one typed action loop for state and asynchronous effects, with component-scoped cancellation and explicit concurrency policies. +Halo gives a PureScript React component one typed action handler plus explicit, component-scoped asynchronous tasks. It is for event-driven UI workflows where plain hooks become hard to coordinate: replace stale searches, prevent overlapping saves, preserve upload order, or retain only the newest pending refresh. -Use Halo when a component has event-driven workflows that are awkward to express as independent hooks: rapid searches that must replace stale requests, saves that must not overlap, ordered uploads, or bursts where only the newest pending action matters. For a single request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. +For one request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. Halo earns its place when actions, state transitions, cancellation, and task concurrency need one coherent owner. -## Install +## Mental model -Halo v4 targets PureScript 0.15.16 and Spago 1.0.4. It is not published yet: the Registry still resolves `react-halo` to v3. To try v4 from a sibling checkout, add Halo as a local package and include `react-basic-dom` for the quick-start renderer: +Halo separates three kinds of work: + +1. **Handlers** react to activation, prop changes, and dispatched actions. They start immediately, are owned by the active React scope, and do not count as task activity. +2. **Tasks** are submitted explicitly with `startTask`. They can outlive the handler that submitted them, use a named concurrency policy, drive `Activity`, and are cancelled on deactivation. +3. **Structured children** are created with `fork`. A child belongs to its current handler or task and is cancelled when that parent finishes. + +An action is an event, not an implicit task. The action handler decides whether to update state immediately, start a task, cancel keyed tasks, subscribe to events, or combine those operations. + +## Try the unreleased v4 + +Halo v4 targets PureScript 0.15.16 and Spago 1.0.4. It is not published yet; the Registry still resolves `react-halo` to v3. Add a sibling checkout as a local package: ```yaml package: dependencies: - - react-basic-dom + - react-basic-dom # only needed by this README's renderer - react-halo workspace: @@ -20,17 +30,17 @@ workspace: path: ../purescript-react-halo ``` -After v4 is published, install both packages with: +After v4 is published: ```console spago install react-halo react-basic-dom ``` -Halo itself does not require `react-basic-dom`; only the example renderer does. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo does not publish an npm runtime entry point. +Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. -## Quick start: a restartable request +## Quick start: replace a stale request -This complete component starts `loadGreeting` when the button is clicked. Clicking again while the request is running cancels the previous Halo task. Even if underlying work cannot be interrupted, the replaced task cannot commit Halo state. +This component handles every click immediately, then explicitly submits a restartable request. A second click fences and cancels the prior `GreetingRequest` task before starting another. ```purescript module Example.LoadButton where @@ -66,19 +76,19 @@ derive instance ordTask :: Ord Task loadButton :: Component Props loadButton = Halo.component "LoadButton" { initialState: \_ -> { loading: false, result: Nothing } - , schedule: \Load -> Halo.Restartable GreetingRequest - , eval: case _ of - Halo.Action Load -> do - modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.props - outcome <- liftAff $ attempt loadGreeting - modify_ _ - { loading = false - , result = Just $ case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting - } - _ -> pure unit + , handlers: Halo.defaultHandlers + { onAction = \Load -> + Halo.startTask (Halo.Restartable GreetingRequest) do + modify_ _ { loading = true, result = Nothing } + Props { loadGreeting } <- Halo.props + outcome <- liftAff $ attempt loadGreeting + modify_ _ + { loading = false + , result = Just $ case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + } + } , onError: \_ error -> Console.error $ "Unexpected Halo failure: " <> message error , render: \{ state, dispatch, activity } -> @@ -97,199 +107,24 @@ loadButton = Halo.component "LoadButton" } ``` -The example catches an expected request failure and stores it in domain state. `onError` is for unexpected failures that escape `eval`. - -## Schedule actions by intent - -The `schedule` function assigns each dispatched action a policy. Keys are an application-defined type with an `Ord` instance; actions with the same key coordinate with one another. - -| Policy | Behavior | -| --- | --- | -| `Every` | Start every action immediately and run them concurrently. It has no key. | -| `Restartable key` | Fence and cancel all running work for `key`, discard its queue, and start the new action. | -| `Drop key` | Ignore the new action while work for `key` is running or queued. | -| `Enqueue key` | Run every action for `key` in first-in, first-out order, one at a time. | -| `KeepLatest key` | Let the running action finish, retain only the newest pending action, and discard intermediate pending actions. | - -A realistic scheduler remains a small pattern match: - -```purescript -data Action - = SearchChanged String - | SaveClicked - | Autosave String - | UploadChunk Int Int - | RecordMetric String - -data Task - = SearchRequest - | SaveRequest - | AutosaveRequest - | Upload Int - -derive instance eqTask :: Eq Task -derive instance ordTask :: Ord Task - -schedule :: Action -> Halo.TaskPolicy Task -schedule = case _ of - SearchChanged _ -> Halo.Restartable SearchRequest - SaveClicked -> Halo.Drop SaveRequest - Autosave _ -> Halo.KeepLatest AutosaveRequest - UploadChunk fileId _ -> Halo.Enqueue (Upload fileId) - RecordMetric _ -> Halo.Every -``` - -Use one stable policy for a given key. Mixing policies on one key is defined by each arriving action, but is harder to reason about. - -`Every` can create unbounded concurrent work, and `Enqueue` can create an unbounded queue if producers are faster than consumers. Use `Drop` or `KeepLatest`, or bound input at its source, when load can spike. - -### Render activity - -`useHalo` and `component` return an `Activity key` snapshot. Activity changes trigger a React render. - -```purescript -let - search = Halo.activityFor SearchRequest activity - total = Halo.activityTotals activity - -in R.text $ - show search.running <> " search running, " <> - show total.queued <> " total queued" -``` - -`activityFor` reports `{ running, queued }` for one keyed task. `activityTotals` includes all keyed work and unkeyed `Every` work. Lifecycle evaluations and structured child fibers are not included. - -## Lifecycle and cancellation - -The evaluator receives: - -```purescript -data Lifecycle props action - = Activate - | Update props -- previous props - | Action action -``` - -`Activate` does **not** mean “exactly once.” React may run an effect setup, cleanup, and setup again for the same hook instance in development StrictMode. Halo treats each setup as a fresh active scope. Deactivation cancels that scope's action evaluations, queued work, lifecycle evaluations, structured children, and subscriptions; a later activation is usable again. - -`Update previousProps` runs when the props reference changes. Read current props with `Halo.props`. Halo keeps the latest evaluator, scheduler, error handler, and React update callbacks rather than permanently capturing the initial hook spec. - -There is no `Finalize` evaluator in v4. React cleanup is synchronous, so asynchronous finalizers would have misleading guarantees. Put external resources behind `subscribe` cleanup, an `Aff` bracket/finalizer, or another resource owner with explicit semantics. - -The task policy applies to dispatched actions, including actions emitted by subscriptions. `Activate` and `Update` evaluations are scope-owned but do not pass through `schedule`. If initialization should use a task policy, dispatch an ordinary action from the application boundary rather than hiding long-running work in lifecycle logic. - -### What cancellation guarantees - -Halo performs two operations on replacement or deactivation: - -1. It marks the old owner inactive immediately, blocking later Halo state commits and capability acquisition. -2. It requests cancellation of the owned `Aff` fibers. - -Cancellation cannot undo an HTTP request already sent, a log already written, or any other external effect already performed. Some foreign async APIs also cannot be interrupted. Model idempotency and server-side concurrency where correctness requires them; Halo's commit fence only protects the component's Halo state from stale work. - -`fork` creates a structured child of the current evaluation. The child is cancelled when its parent finishes, is replaced, or is deactivated. Use it only for concurrency within that evaluation, and use `kill` for earlier cancellation. Returning from the parent is not a way to create a detached component process. - -## Subscriptions - -Halo has a small emitter type rather than depending on Halogen. Registration receives an action callback and must return that receiver's cleanup effect: - -```purescript -eventEmitter = Halo.makeEmitter \emit -> do - listener <- source.listen emit - pure (source.remove listener) - -Halo.Action StartListening -> do - subscriptionId <- Halo.subscribe eventEmitter - modify_ _ { subscriptionId = Just subscriptionId } - -Halo.Action StopListening -> do - { subscriptionId } <- get - traverse_ Halo.unsubscribe subscriptionId - modify_ _ { subscriptionId = Nothing } -``` - -Manual `unsubscribe` removes the cleanup from Halo's tracking before running it. Any cleanup still tracked at deactivation is run automatically. One cleanup failure is reported through `onError` only after Halo has attempted every subscription cleanup and requested cancellation of all other scope-owned work. New subscriptions from stale or inactive evaluations are rejected, and callbacks retained by a misbehaving source remain bound to their original scope rather than targeting a later reactivation. - -An `Emitter` is broadcast-style: every subscriber receives every emitted value. It is not a consuming work queue and provides no backpressure. Each event delivered to Halo is dispatched once and then follows its action policy. Halo v4 intentionally does not expose a coroutine, process, or saga API; task scheduling is the focused concurrency boundary. - -## Error handling - -Every spec must provide: - -```purescript -onError :: Halo.ErrorContext props action -> Error -> Effect Unit -``` - -The context is `ActivationError`, `DeactivationError`, `UpdateError previousProps`, or `ActionError action`. `DeactivationError` reports a subscription cleanup that threw; Halo continues cleaning the rest of the scope before calling `onError`. Expected domain failures belong in the action/state model, usually by catching `Aff` errors inside `eval`. Unexpected uncaught errors go to `onError`. Cancellation caused by replacement or deactivation is suppressed rather than reported as an application failure. - -## Component helper or hook - -Use `Halo.component` when Halo owns the whole component. Its renderer receives: - -```purescript -{ props :: props -, state :: state -, dispatch :: action -> Effect Unit -, activity :: Halo.Activity key -} -``` - -Use `Halo.useHalo` when composing Halo with other React hooks: - -```purescript -halo <- Halo.useHalo - { props - , initialState - , eval - , schedule - , onError - } - --- halo.state --- halo.dispatch --- halo.activity -``` - -`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use normal `get`, `put`, and `modify_`; use `liftAff` for asynchronous work. `Halo.props` reads the latest props. - -`mkEval` remains available for simple lifecycle-to-action routing: +The request catches an expected domain failure and stores it in state. Unexpected failures that escape a handler or task go to `onError` with an `ErrorContext`. -```purescript -eval = Halo.mkEval $ Halo.defaultEval - { initialize = Just InitializeData - , handleAction = handleAction - } -``` +## Learn and reference -The `update` field can map previous props to an optional action. These lifecycle-routed actions execute inside the lifecycle evaluation; they are not independently scheduled. +- [Guide](docs/guide.md): handlers, explicit tasks, policies, cancellation, activity, subscriptions, lifecycle, patterns, and troubleshooting. +- [API reference](docs/reference.md): public types and operations with exact semantics. +- [v3 to v4 migration](docs/migration-v4.md): breaking changes and a practical conversion sequence. -## Migrating from v3 - -Version 4 intentionally breaks the evaluator API to make cancellation and ownership reliable. - -- Change `HaloM props state action m` to `HaloM props state action key`. Halo now runs directly on `Aff`; remove `hoist`, `HaloAp`, and the custom base monad parameter. -- Add an application task-key type with `Eq` and `Ord`, then add `schedule :: action -> TaskPolicy key`. -- Add `onError :: ErrorContext props action -> Error -> Effect Unit`, including the new `DeactivationError` context for subscription cleanup failures. -- Replace `Initialize` with `Activate`. `Activate` is repeatable. -- Remove `Finalize` handlers. Use scoped cancellation, subscriptions, and `Aff` finalizers instead. -- Keep `Update previousProps`, and read current props with `Halo.props`. -- Replace the `useHalo` tuple with the record fields `state`, `dispatch`, and `activity`. -- In `component` renderers, rename `send` to `dispatch` and accept `activity` when needed. -- Revisit `fork`: v4 children are structured under the evaluation that created them, not detached until component unmount. -- Replace `Halogen.Subscription.Emitter` values with `Halo.makeEmitter`; the registration function has the same callback-and-cleanup shape but no Halogen dependency. -- Remove assumptions that action effects run without coordination. Choose `Every` explicitly for v3-like concurrent dispatch. +The important documentation examples compile in `test/Test/Halo/DocExamples.purs`. ## Development -Install the pinned tools and run the checks: - ```console npm ci npm run format:check -npm run build -- --strict +npm run build -- --strict --pedantic-packages npm test +npx spago docs ``` -The runtime tests model React's effect setup-cleanup-setup sequence directly and use deterministic `AVar` gates for scheduling and cancellation. A DOM mounting test is intentionally omitted because this package's npm manifest contains only the PureScript compiler and Spago; the repeatable lifecycle contract is tested at the runtime boundary used by the hook. - -Module documentation is generated by PureScript and can be published to [Pursuit](https://pursuit.purescript.org/packages/purescript-react-halo) with a release. +The deterministic runtime tests model React's effect setup-cleanup-setup sequence directly. A DOM mounting test is intentionally omitted because this library's npm manifest contains only the pinned PureScript compiler and Spago; the hook uses the tested runtime boundary, and the component examples are compile-checked. diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..7a0899b --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,304 @@ +# Halo v4 guide + +This guide explains how to design a component around Halo's explicit action and task model. Start with the [README quick start](../README.md) if you have not built a Halo component yet. Exact signatures are in the [API reference](reference.md). + +## Choose Halo for coordinated component workflows + +React hooks remain the default. Prefer `useAff` when one asynchronous result follows render dependencies and latest-request cancellation is the only coordination you need. + +Halo is useful when a component has a small event protocol and several operations must share state and cancellation rules. Typical examples include: + +- search input where each request replaces the previous request; +- a save button that ignores duplicate clicks; +- per-file upload chunks that must preserve order; +- autosave where the current write may finish but only the newest pending value matters; and +- event sources that dispatch into the same state owner. + +Halo does not provide global state, server caching, a process/saga runtime, or server-side rendering machinery. + +## Think in actions, tasks, and structured children + +### Actions describe events + +Rendering code calls `dispatch :: action -> Effect Unit`. Halo starts `handlers.onAction action` in the active component scope. Every dispatched action gets a handler execution; actions are not deduplicated, queued, or assigned task policies. + +Use an action handler for quick state transitions and decisions: + +```purescript +onAction = case _ of + NameChanged name -> modify_ _ { name = name } + CancelSearch -> Halo.cancelTask SearchRequest + SearchSubmitted query -> + Halo.startTask (Halo.Restartable SearchRequest) (search query) +``` + +Handler execution is scope-owned and commit-fenced after deactivation, but it is not shown in `Activity`. + +### Explicit tasks describe asynchronous work + +`startTask policy computation` submits `computation` to the component task scheduler and returns immediately. The submitted task belongs to the active component scope, not to the handler that submitted it. It can therefore keep running after a successful action handler returns. + +A task may read current props, update Halo state, create structured children, or submit another component-scoped task. Its state commits are fenced when it is replaced or cancelled. + +Make task submission visible where the action is handled. Do not hide it behind a second action-to-policy table; the policy belongs next to the work whose concurrency it controls. + +### Structured children describe parent-bound concurrency + +`fork child` starts `child` concurrently under the current handler or task. Unlike an explicit task, a forked child is cancelled when its parent finishes normally. Use `fork` when the parent remains alive and owns concurrent subwork. Use `startTask` when work must outlive the action handler that launched it. + +```purescript +Halo.startTask (Halo.Restartable Refresh) do + left <- Halo.fork loadLeftPane + right <- Halo.fork loadRightPane + waitUntilReady + Halo.kill left + Halo.kill right +``` + +Returning immediately after `fork` cancels the child; it does not create a detached background process. + +## Configure handlers + +A spec has one cohesive `handlers` record: + +```purescript +type Handlers props state action key = + { onActivate :: HaloM props state action key Unit + , onPropsChange :: props -> HaloM props state action key Unit + , onAction :: action -> HaloM props state action key Unit + } +``` + +Start with `defaultHandlers` and update only the fields you need: + +```purescript +handlers = Halo.defaultHandlers + { onActivate = initializeView + , onPropsChange = \previous -> synchronize previous + , onAction = handleAction + } +``` + +### `onActivate` + +Halo calls `onActivate` for each React effect activation. React development StrictMode can perform setup, cleanup, then setup again for the same hook instance. Treat activation as repeatable, not exactly once. Avoid irreversible “run once” effects unless the external owner supplies idempotency. + +Activation execution is cancelled and commit-fenced on deactivation. If activation submits an explicit task, that task is also component-scoped and is cancelled on deactivation. + +### `onPropsChange previousProps` + +Halo runs this handler when the props reference changes. The argument is the previous props. Read current props inside Halo with: + +```purescript +current <- Halo.props +``` + +The runtime keeps the latest handler record, error handler, and React update callbacks. A handler already running keeps the computation selected when it started. + +### `onAction action` + +Halo starts an action handler as soon as the action is dispatched into the active scope. A subscription emission also dispatches an action through this field. If the component is inactive, dispatch is ignored. + +Long waits in an action handler remain cancellable and do not block other handlers, but they are invisible to `Activity`. Prefer an explicit task when running/queued state or a concurrency policy matters. + +## Work with state and props + +`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use normal state operations: + +```purescript +current <- get +modify_ _ { status = Loading } +put next +``` + +Halo mirrors committed state into React. A replaced or deactivated owner can still finish foreign work, but later `get`/`put`/`modify_` operations cannot commit stale Halo state. + +`Halo.props` reads the latest props rather than a render-time snapshot. Capture a value before starting a task when the task must use the value associated with the action: + +```purescript +onAction Submit = do + { form } <- Halo.props + Halo.startTask (Halo.Drop SubmitRequest) (submit form) +``` + +## Select a task policy + +### `Every` + +Starts every submitted task immediately. Tasks run concurrently and appear only in total activity because `Every` has no key. + +Use it for independent bounded work such as metrics. A high-rate producer can create unbounded concurrency. + +### `Restartable key` + +Synchronously fences all running tasks for `key`, discards queued tasks, requests cancellation, then starts the new task. Use it for latest-request-wins search and navigation. + +Cancellation cannot undo an external effect that already happened. If server ordering matters, add idempotency or version checks at that boundary. + +### `Drop key` + +Starts the task only when `key` has no running or queued work. Otherwise the submission is discarded and `startTask` still returns normally. Use it to prevent duplicate form submissions. + +### `Enqueue key` + +Runs every task for `key` first-in, first-out, one at a time. Different keys remain independent. Use it for ordered writes or per-resource uploads. + +The queue is unbounded. Bound the producer or choose another policy when sustained input can exceed throughput. + +### `KeepLatest key` + +Lets the current task finish, keeps only the newest queued task, and discards intermediate queued submissions. Use it for autosave when cancelling an in-flight write is undesirable but stale pending writes have no value. + +Use one stable policy for a given key. Mixing policies is processed according to each arriving submission, but it makes the workflow harder to reason about. + +## Cancel keyed tasks explicitly + +`cancelTask key` immediately fences running tasks for the key, discards its queue, requests fiber cancellation, updates activity, and returns. It does not affect unkeyed `Every` tasks. + +```purescript +onAction = case _ of + SearchChanged query -> + Halo.startTask (Halo.Restartable SearchRequest) (search query) + SearchCleared -> do + Halo.cancelTask SearchRequest + modify_ _ { results = [] } +``` + +If a keyed task cancels its own key, it fences and requests cancellation of itself as well as its keyed siblings. + +## Render task activity + +`component` renderers and `useHalo` return `Activity key`. Activity updates cause React renders. + +```purescript +let + search = Halo.activityFor SearchRequest halo.activity + total = Halo.activityTotals halo.activity +``` + +Each count has `{ running, queued }`. Totals include keyed tasks and unkeyed `Every` tasks. Per-key counts include keyed tasks only. + +Activity deliberately excludes: + +- activation, prop-change, and action handlers; +- structured `fork` children; and +- emitter subscription cleanup. + +This keeps the value precise: it represents only work submitted through `startTask`. + +## Subscribe to custom emitters + +Halo's small emitter type avoids a Halogen dependency: + +```purescript +events :: Halo.Emitter Action +events = Halo.makeEmitter \emit -> do + listener <- source.listen emit + pure (source.remove listener) +``` + +Registration receives an action callback and returns an `Effect Unit` cleanup. Subscribe inside Halo: + +```purescript +subscriptionId <- Halo.subscribe events +Halo.unsubscribe subscriptionId +``` + +A subscription remains component-scoped after the creating handler finishes. Manual unsubscription removes cleanup from tracking before running it. Deactivation attempts every tracked cleanup, even when one throws, and reports each thrown cleanup as `DeactivationError` after cancellation requests have been issued. + +`Emitter` is broadcast-style. It is not a consuming queue and does not provide backpressure. Each emission dispatches one action to each registered Halo receiver. Choose task policies inside `onAction` when emitted actions start asynchronous work. + +## Handle unexpected errors + +Every spec supplies: + +```purescript +onError :: ErrorContext props action key -> Error -> Effect Unit +``` + +Contexts are: + +- `ActivationError` for `onActivate`; +- `PropsChangeError previousProps` for `onPropsChange`; +- `ActionError action` for `onAction`; +- `TaskError policy` for an explicit task; and +- `DeactivationError` for a throwing subscription cleanup. + +Expected failures belong in domain state or actions. Catch them inside the task with `attempt`, `try`, or a domain-specific error type. Let genuinely unexpected failures reach `onError` for logging or reporting. Cancellation requested by Halo is suppressed rather than reported as an application error. + +## Understand cancellation limits + +Replacement and deactivation do two things: + +1. mark the old owner inactive synchronously, blocking later Halo state commits and new Halo-owned capabilities; and +2. request cancellation of its `Aff` fibers. + +Cancellation is cooperative. It cannot retract an HTTP request, storage write, analytics event, or foreign callback already performed. Commit fencing protects Halo state, not external systems. Design external operations for retry, ordering, and idempotency when those properties matter. + +## Choose `component` or `useHalo` + +Use `Halo.component` when Halo owns the full component. The renderer receives props, state, dispatch, and activity. + +Use `Halo.useHalo` when composing with other hooks: + +```purescript +halo <- Halo.useHalo + { props + , initialState + , handlers + , onError + } +``` + +Read `halo.state`, call `halo.dispatch`, and render `halo.activity`. + +## Common patterns + +### Search with latest-request-wins + +Capture the query from the action and use `Restartable`: + +```purescript +SearchChanged query -> + Halo.startTask (Halo.Restartable SearchRequest) do + results <- liftAff $ fetchResults query + modify_ _ { query = query, results = results } +``` + +### Ignore duplicate saves + +```purescript +SaveClicked -> + Halo.startTask (Halo.Drop SaveRequest) saveCurrentForm +``` + +### Ordered work per resource + +```purescript +UploadChunk fileId chunk -> + Halo.startTask (Halo.Enqueue (Upload fileId)) (upload chunk) +``` + +### Cancel when input becomes empty + +```purescript +QueryChanged "" -> Halo.cancelTask SearchRequest +QueryChanged query -> + Halo.startTask (Halo.Restartable SearchRequest) (search query) +``` + +## Troubleshooting and footguns + +**My fork stops immediately.** Its parent returned. Use `startTask` for component-scoped work, or keep the parent alive while it owns the child. + +**Activity is zero while work is running.** The work is probably in a handler or structured child. Submit it with `startTask` if it is task activity. + +**A dropped task did not run an error or completion action.** `Drop` intentionally discards the computation when its key is busy. Put only optional work behind it, or reflect acceptance separately in state. + +**My queue keeps growing.** `Enqueue` has no built-in bound. Limit input, batch it, or use `KeepLatest`/`Drop`. + +**A cancelled request still reached the server.** Halo can fence component commits and request `Aff` cancellation; it cannot undo an external side effect. + +**Initialization ran twice in development.** React StrictMode replayed effect activation. Make `onActivate` replay-safe. + +**An emitter overwhelms the component.** Emitters broadcast without backpressure. Reduce events at the source or let actions submit tasks with a pressure-appropriate policy. diff --git a/docs/migration-v4.md b/docs/migration-v4.md new file mode 100644 index 0000000..3f8de6e --- /dev/null +++ b/docs/migration-v4.md @@ -0,0 +1,168 @@ +# Migrate from Halo v3 to v4 + +Halo v4 is an unreleased breaking redesign. It replaces the Free/FreeAp evaluator and implicit action effects with a direct scoped runtime, named handlers, and explicit tasks. There are no compatibility aliases in v4. + +## Why the model changed + +In v3, `eval` combined lifecycle events and actions, and action evaluation commonly became asynchronous work by convention. That made it difficult to tell whether an action was an event, a long-running task, or both. It also left concurrency policy and cancellation ownership implicit. + +In v4: + +- handlers respond to lifecycle and action events; +- `startTask` explicitly marks component-scoped asynchronous work; +- a `TaskPolicy` is chosen next to that work; +- `fork` is explicitly parent-scoped; and +- activity counts explicit tasks only. + +## Migration sequence + +### 1. Change `HaloM` + +Replace: + +```purescript +HaloM props state action m a +``` + +with: + +```purescript +HaloM props state action key a +``` + +Choose an application task-key type with an `Ord` instance. Halo now runs directly on `Aff`; remove the custom base monad parameter, `hoist`, `HaloAp`, and Free/FreeAp-specific code. Use `liftAff` for asynchronous effects. + +### 2. Replace `eval` with `handlers` + +Replace lifecycle pattern matching: + +```purescript +eval = case _ of + Initialize -> initialize + Update previous -> synchronize previous + Action action -> handleAction action + Finalize -> finalize +``` + +with: + +```purescript +handlers = Halo.defaultHandlers + { onActivate = initialize + , onPropsChange = synchronize + , onAction = handleAction + } +``` + +There is no public `Lifecycle`, `EvalSpec`, `mkEval`, or `defaultEval` in v4. + +`onActivate` is repeatable under React StrictMode. There is no asynchronous deactivation handler: React cleanup is synchronous, and pretending otherwise would give misleading completion guarantees. Use subscription cleanup, `Aff` finalizers, or an external resource owner. + +### 3. Make tasks explicit + +In v3, an action handler might perform a request directly: + +```purescript +Action (SearchChanged query) -> do + results <- liftAff $ search query + modify_ _ { results = results } +``` + +In v4, submit work with its policy: + +```purescript +onAction = case _ of + SearchChanged query -> + Halo.startTask (Halo.Restartable SearchRequest) do + results <- liftAff $ search query + modify_ _ { results = results } +``` + +Delete any top-level `schedule :: action -> TaskPolicy key`. An action is no longer implicitly a task. Some actions may only modify state; others may submit multiple tasks or cancel a keyed task. + +### 4. Add explicit keyed cancellation where needed + +Replace stored task fibers or cancellation actions with: + +```purescript +Halo.cancelTask SearchRequest +``` + +This cancels running keyed tasks and discards their queue. It does not affect `Every` tasks. + +### 5. Update the error handler + +Change: + +```purescript +onError :: ErrorContext props action -> Error -> Effect Unit +``` + +into: + +```purescript +onError :: ErrorContext props action key -> Error -> Effect Unit +``` + +Handle the v4 contexts: + +- `ActivationError`; +- `DeactivationError` for subscription cleanup; +- `PropsChangeError previousProps`; +- `ActionError action`; and +- `TaskError policy`. + +Expected request failures still belong in domain state or actions. + +### 6. Update hook and component specs + +Remove `eval` and `schedule`; add `handlers`: + +```purescript +halo <- Halo.useHalo + { props + , initialState + , handlers + , onError + } +``` + +`useHalo` returns a record with `state`, `dispatch`, and `activity`. + +`Halo.component` renderers receive `{ props, state, dispatch, activity }`. The old `send` field is now `dispatch`. + +### 7. Revisit every `fork` + +A v4 `fork` is a structured child. It is cancelled when its creating handler or task finishes. If the old code expected a fork to survive handler completion until component unmount, convert it to an explicit task: + +```purescript +Halo.startTask (Halo.Restartable BackgroundSync) backgroundSync +``` + +Use `fork` only for concurrency owned by a parent that remains alive. + +### 8. Replace Halogen emitters + +Halo v4 has its own small emitter type: + +```purescript +events = Halo.makeEmitter \emit -> do + listener <- source.listen emit + pure (source.remove listener) +``` + +`subscribe`, `subscribe'`, and `unsubscribe` remain. Manual cleanup is removed from tracking before it runs; scope cleanup failures are isolated and reported through `DeactivationError`. + +## Behavior changes to verify + +Before completing a migration, verify: + +- `onActivate` is safe to replay; +- each long-running operation uses an intentional policy; +- `Drop` submissions are genuinely optional; +- `Enqueue` producers cannot grow an unbounded queue unexpectedly; +- `cancelTask` is used when UI state must clear keyed work without replacement; +- activity-dependent UI expects explicit tasks only; +- structured children do not need to outlive their parents; +- expected failures are modeled in state rather than logged as unexpected errors; and +- external writes remain correct even when local cancellation cannot undo them. diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..fe0db03 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,212 @@ +# Halo v4 API reference + +Import the intentional public surface from `React.Halo`: + +```purescript +import React.Halo as Halo +``` + +Runtime constructors and ownership records are internal and not exported from this module. + +## Core computation + +```purescript +HaloM props state action key a +``` + +`HaloM` runs directly on `Aff` in a private scoped environment. It has `Functor`, `Apply`, `Applicative`, `Bind`, `Monad`, `MonadState state`, `MonadEffect`, and `MonadAff` instances. + +Type parameters: + +- `props`: current React component props; +- `state`: Halo-owned component state; +- `action`: events accepted by `dispatch` and subscriptions; +- `key`: application-defined explicit task keys; and +- `a`: computation result. + +## Handlers + +```purescript +type Handlers props state action key = + { onActivate :: HaloM props state action key Unit + , onAction :: action -> HaloM props state action key Unit + , onPropsChange :: props -> HaloM props state action key Unit + } + +defaultHandlers :: forall props state action key. Handlers props state action key +``` + +`defaultHandlers` ignores all callbacks. Use PureScript record update syntax to replace selected fields. + +- `onActivate` runs for every React effect activation and may run more than once for one hook instance. +- `onAction` starts for each action dispatched while the scope is active. +- `onPropsChange previousProps` starts when the props reference changes. Read current props with `props`. + +Handlers are scope-owned, concurrent, and excluded from `Activity`. + +## Task submission and cancellation + +```purescript +startTask + :: Ord key + => TaskPolicy key + -> HaloM props state action key Unit + -> HaloM props state action key Unit + +cancelTask + :: Ord key + => key + -> HaloM props state action key Unit +``` + +`startTask` submits component-scoped work and returns without waiting for it. The task outlives successful completion of the submitting handler or task. It is cancelled on scope deactivation. + +`cancelTask key` synchronously fences running tasks for `key`, discards the queue, requests cancellation, publishes new activity, and returns. It cannot target unkeyed `Every` tasks. + +```purescript +data TaskPolicy key + = Every + | Restartable key + | Drop key + | Enqueue key + | KeepLatest key +``` + +- `Every`: starts all submissions concurrently. +- `Restartable key`: replaces running and queued work for the key. +- `Drop key`: discards a submission while the key is busy. +- `Enqueue key`: runs every submission FIFO, one at a time. +- `KeepLatest key`: lets current work finish and retains only the newest queued submission. + +## Activity + +```purescript +type TaskCounts = + { running :: Int + , queued :: Int + } + +activityTotals :: Activity key -> TaskCounts +activityFor :: Ord key => key -> Activity key -> TaskCounts +emptyActivity :: Activity key +``` + +`Activity` counts explicit `startTask` submissions only. Totals include unkeyed and keyed tasks. `activityFor` reports one keyed slot. Handler execution and structured children are excluded. + +## State and props + +```purescript +props :: HaloM props state action key props +``` + +Use `MonadState` operations for state. `props` returns the latest component props. State mutation and capability acquisition are commit-fenced when the current owner becomes stale. + +## Structured children + +```purescript +fork + :: HaloM props state action key Unit + -> HaloM props state action key ForkId + +kill + :: ForkId + -> HaloM props state action key Unit +``` + +`fork` creates a concurrent child owned by the current handler or task. Parent completion or cancellation cancels the child. `kill` requests earlier cancellation. `ForkId` is abstract from `React.Halo`. + +## Subscriptions and emitters + +```purescript +makeEmitter + :: ((action -> Effect Unit) -> Effect (Effect Unit)) + -> Emitter action + +subscribe + :: Ord key + => Emitter action + -> HaloM props state action key SubscriptionId + +subscribe' + :: Ord key + => (SubscriptionId -> Emitter action) + -> HaloM props state action key SubscriptionId + +unsubscribe + :: SubscriptionId + -> HaloM props state action key Unit +``` + +Emitter registration receives a receiver and returns its cleanup effect. Subscription emissions dispatch actions into the activation scope that registered them. Stale callbacks cannot target a later scope. + +Manual unsubscription removes tracking before cleanup runs. Deactivation attempts all remaining cleanup effects; throwing cleanup is reported as `DeactivationError` without preventing other cleanup and cancellation requests. + +`SubscriptionId` is abstract from `React.Halo`. + +## Errors + +```purescript +data ErrorContext props action key + = ActivationError + | DeactivationError + | PropsChangeError props + | ActionError action + | TaskError (TaskPolicy key) +``` + +Every hook or component spec supplies: + +```purescript +onError :: ErrorContext props action key -> Error -> Effect Unit +``` + +Halo sends unexpected handler and task failures to this callback. `DeactivationError` is reserved for throwing subscription cleanup. Halo suppresses cancellation errors it initiated. + +## Hook API + +```purescript +type HookSpec props state action key = + { handlers :: Handlers props state action key + , initialState :: state + , onError :: ErrorContext props action key -> Error -> Effect Unit + , props :: props + } + +type HaloHook state action key = + { activity :: Activity key + , dispatch :: action -> Effect Unit + , state :: state + } + +useHalo + :: Ord key + => HookSpec props state action key + -> Hook (UseHalo props state action key) (HaloHook state action key) +``` + +The hook synchronizes the latest handlers and callbacks on each React effect cycle. Activation cleanup deactivates the owned scope; StrictMode reactivation creates a fresh scope. + +## Component API + +```purescript +type ComponentSpec props state action key = + { handlers :: Handlers props state action key + , initialState :: props -> state + , onError :: ErrorContext props action key -> Error -> Effect Unit + , render :: + { activity :: Activity key + , dispatch :: action -> Effect Unit + , props :: props + , state :: state + } + -> JSX + } + +component + :: Ord key + => String + -> ComponentSpec props state action key + -> Component props +``` + +Use `component` when Halo owns the entire component. Use `useHalo` when other React hooks share the render function. diff --git a/src/React/Halo.purs b/src/React/Halo.purs index 824a9c1..9a37038 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -3,8 +3,8 @@ module React.Halo ) where import React.Halo.Component (ComponentSpec, component) as Exports +import React.Halo.Handlers (Handlers, defaultHandlers) as Exports import React.Halo.Hook (HaloHook, HookSpec, UseHalo(..), useHalo) as Exports -import React.Halo.Internal.Eval (EvalSpec, defaultEval, mkEval) as Exports -import React.Halo.Internal.Runtime (HaloM, fork, kill, props, subscribe, subscribe', unsubscribe) as Exports -import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, Lifecycle(..), SubscriptionId, TaskCounts, TaskPolicy(..), activityFor, activityTotals, emptyActivity) as Exports +import React.Halo.Internal.Runtime (HaloM, cancelTask, fork, kill, props, startTask, subscribe, subscribe', unsubscribe) as Exports +import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, SubscriptionId, TaskCounts, TaskPolicy(..), activityFor, activityTotals, emptyActivity) as Exports import React.Halo.Subscription (Emitter, makeEmitter) as Exports diff --git a/src/React/Halo/Component.purs b/src/React/Halo/Component.purs index 33f5408..b75a278 100644 --- a/src/React/Halo/Component.purs +++ b/src/React/Halo/Component.purs @@ -9,14 +9,15 @@ import Effect (Effect) import Effect.Aff (Error) import React.Basic.Hooks (Component, JSX) import React.Basic.Hooks as React +import React.Halo.Handlers (Handlers) import React.Halo.Hook (useHalo) -import React.Halo.Internal.Runtime (HaloM) -import React.Halo.Internal.Types (Activity, ErrorContext, Lifecycle, TaskPolicy) +import React.Halo.Internal.Types (Activity, ErrorContext) +-- | Complete configuration for a Halo-owned React component. type ComponentSpec props state action key = - { eval :: Lifecycle props action -> HaloM props state action key Unit + { handlers :: Handlers props state action key , initialState :: props -> state - , onError :: ErrorContext props action -> Error -> Effect Unit + , onError :: ErrorContext props action key -> Error -> Effect Unit , render :: { activity :: Activity key , dispatch :: action -> Effect Unit @@ -24,10 +25,9 @@ type ComponentSpec props state action key = , state :: state } -> JSX - , schedule :: action -> TaskPolicy key } --- | Build a complete React component around a Halo action runtime. +-- | Build a complete React component around a Halo action and task runtime. component :: forall props state action key . Ord key @@ -38,11 +38,10 @@ component name spec = React.component name \props -> React.do initialState <- React.useMemo unit \_ -> spec.initialState props halo <- useHalo - { eval: spec.eval + { handlers: spec.handlers , initialState , onError: spec.onError , props - , schedule: spec.schedule } pure $ spec.render { activity: halo.activity diff --git a/src/React/Halo/Handlers.purs b/src/React/Halo/Handlers.purs new file mode 100644 index 0000000..6069ddd --- /dev/null +++ b/src/React/Halo/Handlers.purs @@ -0,0 +1,25 @@ +module React.Halo.Handlers + ( Handlers + , defaultHandlers + ) where + +import Prelude + +import React.Halo.Internal.Runtime (Handlers) as Runtime + +-- | Activation, prop-change, and action callbacks for a Halo component. +-- | +-- | `onActivate` may run again after React replays an effect setup. It is not an +-- | exactly-once mount callback. `onPropsChange` receives the previous props; +-- | use `React.Halo.props` to read the current props. `onAction` starts +-- | immediately for every dispatched action. +type Handlers props state action key = Runtime.Handlers props state action key + +-- | Handlers that do nothing. Use a record update to configure only the +-- | callbacks a component needs. +defaultHandlers :: forall props state action key. Handlers props state action key +defaultHandlers = + { onActivate: pure unit + , onAction: \_ -> pure unit + , onPropsChange: \_ -> pure unit + } diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index c0ad10f..4ac0f7d 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -14,20 +14,22 @@ import Effect.Aff (Error) import Effect.Unsafe (unsafePerformEffect) import React.Basic.Hooks (Hook, UseEffect, UseMemo, UseState) import React.Basic.Hooks as React -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, syncSpec, updateProps) -import React.Halo.Internal.Types (Activity, ErrorContext, Lifecycle, TaskPolicy, emptyActivity) +import React.Halo.Handlers (Handlers) +import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, syncSpec, updateProps) +import React.Halo.Internal.Types (Activity, ErrorContext, emptyActivity) --- | Configuration for `useHalo`. The task key is chosen by the application and --- | only needs an `Ord` instance. +-- | Configuration for `useHalo`. +-- | +-- | The application chooses `key`; only explicit tasks use it, and it needs an +-- | `Ord` instance so Halo can coordinate keyed task slots. type HookSpec props state action key = - { eval :: Lifecycle props action -> HaloM props state action key Unit + { handlers :: Handlers props state action key , initialState :: state - , onError :: ErrorContext props action -> Error -> Effect Unit + , onError :: ErrorContext props action key -> Error -> Effect Unit , props :: props - , schedule :: action -> TaskPolicy key } --- | Values exposed to rendering code. +-- | State, action dispatch, and explicit task activity exposed to rendering code. type HaloHook state action key = { activity :: Activity key , dispatch :: action -> Effect Unit @@ -50,12 +52,15 @@ newtype UseHalo props state action key hooks = UseHalo derive instance newtypeUseHalo :: Newtype (UseHalo props state action key hooks) _ -- | Run Halo inside a `react-basic-hooks` component. +-- | +-- | React effect activation owns the runtime scope. Cleanup deactivates it, and +-- | a later StrictMode replay creates a fresh usable scope. useHalo :: forall props state action key . Ord key => HookSpec props state action key -> Hook (UseHalo props state action key) (HaloHook state action key) -useHalo { props, initialState, eval, schedule, onError } = +useHalo { props, initialState, handlers, onError } = React.coerceHook React.do state /\ setState <- React.useState' initialState activity /\ setActivity <- React.useState' emptyActivity @@ -64,13 +69,13 @@ useHalo { props, initialState, eval, schedule, onError } = { activityUpdate: setActivity , initialProps: props , initialState - , spec: { eval, schedule, onError } + , spec: { handlers, onError } , stateUpdate: setState } React.useEffectAlways do syncSpec runtime { activityUpdate: setActivity - , spec: { eval, schedule, onError } + , spec: { handlers, onError } , stateUpdate: setState } pure mempty diff --git a/src/React/Halo/Internal/Eval.purs b/src/React/Halo/Internal/Eval.purs deleted file mode 100644 index 8e8ffb9..0000000 --- a/src/React/Halo/Internal/Eval.purs +++ /dev/null @@ -1,38 +0,0 @@ -module React.Halo.Internal.Eval - ( EvalSpec - , defaultEval - , mkEval - ) where - -import Prelude - -import Data.Foldable (traverse_) -import Data.Maybe (Maybe(..)) -import React.Halo.Internal.Runtime (HaloM) -import React.Halo.Internal.Types (Lifecycle(..)) - --- | Convenience configuration for routing lifecycle events into the same action --- | handler used by dispatched actions. -type EvalSpec props state action key = - { handleAction :: action -> HaloM props state action key Unit - , initialize :: Maybe action - , update :: props -> Maybe action - } - --- | An evaluator that ignores activation and prop updates until configured. -defaultEval :: forall props state action key. EvalSpec props state action key -defaultEval = - { handleAction: \_ -> pure unit - , initialize: Nothing - , update: \_ -> Nothing - } - -mkEval - :: forall props state action key - . EvalSpec props state action key - -> Lifecycle props action - -> HaloM props state action key Unit -mkEval eval = case _ of - Activate -> traverse_ eval.handleAction eval.initialize - Update previousProps -> traverse_ eval.handleAction (eval.update previousProps) - Action action -> eval.handleAction action diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index 9342e8e..9b28bef 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -1,15 +1,16 @@ module React.Halo.Internal.Runtime ( HaloM + , Handlers , Runtime - , RuntimeSpec , activate + , cancelTask , createRuntime , deactivate , dispatch , fork , kill , props - , runForTest + , startTask , subscribe , subscribe' , syncSpec @@ -39,7 +40,7 @@ import Effect.Class (class MonadEffect, liftEffect) import Effect.Exception as Exception import Effect.Ref (Ref) import Effect.Ref as Ref -import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), Lifecycle(..), SubscriptionId(..), TaskPolicy(..), emptyActivity) +import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), SubscriptionId(..), TaskPolicy(..), emptyActivity) import React.Halo.Subscription (Emitter) import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) @@ -57,10 +58,17 @@ derive newtype instance monadHaloM :: Monad (HaloM props state action key) derive newtype instance monadEffectHaloM :: MonadEffect (HaloM props state action key) derive newtype instance monadAffHaloM :: MonadAff (HaloM props state action key) +-- | Activation, prop-change, and action callbacks. Handlers run immediately as scope-owned +-- | computations; only work submitted with `startTask` enters the task scheduler. +type Handlers props state action key = + { onActivate :: HaloM props state action key Unit + , onAction :: action -> HaloM props state action key Unit + , onPropsChange :: props -> HaloM props state action key Unit + } + type RuntimeSpec props state action key = - { eval :: Lifecycle props action -> HaloM props state action key Unit - , onError :: ErrorContext props action -> Error -> Effect Unit - , schedule :: action -> TaskPolicy key + { handlers :: Handlers props state action key + , onError :: ErrorContext props action key -> Error -> Effect Unit } newtype Runtime props state action key = Runtime @@ -83,9 +91,15 @@ newtype Scope props state action key = Scope , tasks :: Ref (Map key (TaskSlot props state action key)) } +type TaskRequest :: Type -> Type -> Type -> Type -> Type +type TaskRequest props state action key = + { computation :: HaloM props state action key Unit + , policy :: TaskPolicy key + } + type TaskSlot :: Type -> Type -> Type -> Type -> Type type TaskSlot props state action key = - { queued :: Array action + { queued :: Array (TaskRequest props state action key) , running :: Map Int (Root props state action key) } @@ -103,7 +117,7 @@ newtype Root props state action key = Root } type Execution props state action key = - { context :: ErrorContext props action + { context :: ErrorContext props action key , owner :: Owner props state action key , runtime :: Runtime props state action key , scope :: Scope props state action key @@ -189,7 +203,7 @@ activate runtime@(Runtime state) = do let scope = Scope { active, every, generation, roots, subscriptions, tasks } Ref.write (Just scope) state.scope spec <- Ref.read state.spec - startLifecycle runtime scope ActivationError (spec.eval Activate) + startHandler runtime scope ActivationError spec.handlers.onActivate deactivate :: forall props state action key. Ord key => Runtime props state action key -> Effect Unit deactivate runtime@(Runtime state) = do @@ -236,7 +250,7 @@ updateProps runtime@(Runtime state) newProps = do traverse_ ( \scope -> do spec <- Ref.read state.spec - startLifecycle runtime scope (UpdateError previousProps) (spec.eval (Update previousProps)) + startHandler runtime scope (PropsChangeError previousProps) (spec.handlers.onPropsChange previousProps) ) activeScope @@ -261,14 +275,46 @@ dispatchToScope runtime@(Runtime state) scope action = do current <- isScopeCurrent runtime scope when current do spec <- Ref.read state.spec - schedule runtime scope (spec.schedule action) action + startHandler runtime scope (ActionError action) (spec.handlers.onAction action) +-- | Read the latest component props. props :: forall props state action key. HaloM props state action key props props = HaloM do execution <- ask let Runtime runtime = execution.runtime liftEffect $ Ref.read runtime.props +-- | Submit a component-scoped task and return immediately. The task is owned by +-- | the active scope rather than by the handler or task that submitted it. +startTask + :: forall props state action key + . Ord key + => TaskPolicy key + -> HaloM props state action key Unit + -> HaloM props state action key Unit +startTask policy computation = HaloM do + execution <- ask + liftEffect do + current <- isCurrent execution + when current $ + scheduleTask execution.runtime execution.scope { computation, policy } + +-- | Fence and cancel running tasks for a key and discard every queued task for +-- | that key. Unkeyed `Every` tasks are unaffected. +cancelTask + :: forall props state action key + . Ord key + => key + -> HaloM props state action key Unit +cancelTask key = HaloM do + execution <- ask + liftEffect do + current <- isCurrent execution + when current $ + cancelKeyedTasks execution.runtime execution.scope key + +-- | Register an emitter in the active component scope. Its cleanup runs on +-- | manual unsubscription or scope deactivation. subscribe :: forall props state action key . Ord key @@ -276,6 +322,7 @@ subscribe -> HaloM props state action key SubscriptionId subscribe = subscribe' <<< const +-- | Like `subscribe`, but provide the allocated identifier to the emitter. subscribe' :: forall props state action key . Ord key @@ -292,6 +339,8 @@ subscribe' makeEmitter = HaloM do Ref.modify_ (Map.insert sid cleanup) scope.subscriptions pure sid +-- | Remove a tracked subscription before running its cleanup. A throwing +-- | cleanup therefore cannot be retried during deactivation. unsubscribe :: forall props state action key . SubscriptionId @@ -311,6 +360,8 @@ unsubscribe sid = HaloM do scope.subscriptions traverse_ identity subscription +-- | Start a structured child of the current handler or task. The child is +-- | cancelled when its parent finishes or is cancelled. fork :: forall props state action key . HaloM props state action key Unit @@ -329,6 +380,7 @@ fork child = HaloM do prepared.start pure fid +-- | Cancel a structured child before its parent finishes. kill :: forall props state action key . ForkId @@ -345,63 +397,59 @@ kill fid = HaloM do parent.children traverse_ (liftAff <<< cancelRootAff) child --- | Internal test seam: run one computation in a currently active scope. This --- | is not re-exported by `React.Halo`. -runForTest +startHandler :: forall props state action key - . Ord key - => Runtime props state action key - -> ErrorContext props action - -> HaloM props state action key Unit - -> Effect Unit -runForTest runtime@(Runtime state) context computation = do - activeScope <- Ref.read state.scope - traverse_ (\scope -> startLifecycle runtime scope context computation) activeScope - -startLifecycle - :: forall props state action key - . Ord key - => Runtime props state action key + . Runtime props state action key -> Scope props state action key - -> ErrorContext props action + -> ErrorContext props action key -> HaloM props state action key Unit -> Effect Unit -startLifecycle runtime scope@(Scope current) context computation = do +startHandler runtime scope@(Scope current) context computation = do runId <- fresh runtime prepared <- prepare Nothing runtime scope context computation \_ -> Ref.modify_ (Map.delete runId) current.roots Ref.modify_ (Map.insert runId prepared.root) current.roots prepared.start -schedule +scheduleTask :: forall props state action key . Ord key => Runtime props state action key -> Scope props state action key - -> TaskPolicy key - -> action + -> TaskRequest props state action key -> Effect Unit -schedule runtime scope@(Scope current) policy action = case policy of +scheduleTask runtime scope@(Scope current) request = case request.policy of Every -> do runId <- fresh runtime - prepared <- prepare Nothing runtime scope (ActionError action) (evalAction runtime action) \_ -> do + prepared <- prepare Nothing runtime scope (TaskError request.policy) request.computation \_ -> do Ref.modify_ (Map.delete runId) current.every notifyActivity runtime scope Ref.modify_ (Map.insert runId prepared.root) current.every notifyActivity runtime scope prepared.start Restartable key -> do - tasks <- Ref.read current.tasks - let previous = maybe mempty (Map.values <<< _.running) (Map.lookup key tasks) - Ref.modify_ (Map.insert key { running: Map.empty, queued: [] }) current.tasks - traverse_ cancelRoot previous - startKeyed runtime scope key action + cancelKeyedTasks runtime scope key + startKeyed runtime scope key request Drop key -> do tasks <- Ref.read current.tasks let busy = maybe false (\slot -> not Map.isEmpty slot.running || not Array.null slot.queued) (Map.lookup key tasks) - unless busy $ startKeyed runtime scope key action - Enqueue key -> enqueueOrStart runtime scope key action false - KeepLatest key -> enqueueOrStart runtime scope key action true + unless busy $ startKeyed runtime scope key request + Enqueue key -> enqueueOrStart runtime scope key request false + KeepLatest key -> enqueueOrStart runtime scope key request true + +cancelKeyedTasks + :: forall props state action key + . Ord key + => Runtime props state action key + -> Scope props state action key + -> key + -> Effect Unit +cancelKeyedTasks runtime scope@(Scope current) key = do + tasks <- Ref.read current.tasks + let previous = maybe mempty (Map.values <<< _.running) (Map.lookup key tasks) + Ref.modify_ (Map.delete key) current.tasks + traverse_ cancelRoot previous + notifyActivity runtime scope startKeyed :: forall props state action key @@ -409,11 +457,11 @@ startKeyed => Runtime props state action key -> Scope props state action key -> key - -> action + -> TaskRequest props state action key -> Effect Unit -startKeyed runtime scope@(Scope current) key action = do +startKeyed runtime scope@(Scope current) key request = do runId <- fresh runtime - prepared <- prepare Nothing runtime scope (ActionError action) (evalAction runtime action) \_ -> + prepared <- prepare Nothing runtime scope (TaskError request.policy) request.computation \_ -> completeKeyed runtime scope key runId Ref.modify_ (Map.alter (Just <<< addRun runId prepared.root <<< maybe emptySlot identity) key) current.tasks notifyActivity runtime scope @@ -425,17 +473,17 @@ enqueueOrStart => Runtime props state action key -> Scope props state action key -> key - -> action + -> TaskRequest props state action key -> Boolean -> Effect Unit -enqueueOrStart runtime scope@(Scope current) key action keepOnlyLatest = do +enqueueOrStart runtime scope@(Scope current) key request keepOnlyLatest = do tasks <- Ref.read current.tasks case Map.lookup key tasks of Just slot | not Map.isEmpty slot.running -> do - let queued = if keepOnlyLatest then [ action ] else Array.snoc slot.queued action + let queued = if keepOnlyLatest then [ request ] else Array.snoc slot.queued request Ref.modify_ (Map.insert key (slot { queued = queued })) current.tasks notifyActivity runtime scope - _ -> startKeyed runtime scope key action + _ -> startKeyed runtime scope key request completeKeyed :: forall props state action key @@ -469,7 +517,7 @@ prepare . Maybe (Owner props state action key) -> Runtime props state action key -> Scope props state action key - -> ErrorContext props action + -> ErrorContext props action key -> HaloM props state action key Unit -> (Owner props state action key -> Effect Unit) -> Effect (Prepared props state action key) @@ -503,16 +551,6 @@ runHaloM -> Aff a runHaloM execution (HaloM computation) = runReaderT computation execution -evalAction - :: forall props state action key - . Runtime props state action key - -> action - -> HaloM props state action key Unit -evalAction (Runtime runtime) action = HaloM do - spec <- liftEffect $ Ref.read runtime.spec - let HaloM computation = spec.eval (Action action) - computation - createOwner :: forall props state action key . Maybe (Owner props state action key) diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index cfd8055..e310610 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -2,7 +2,6 @@ module React.Halo.Internal.Types ( Activity(..) , ErrorContext(..) , ForkId(..) - , Lifecycle(..) , SubscriptionId(..) , TaskCounts , TaskPolicy(..) @@ -17,18 +16,13 @@ import Data.Map (Map) import Data.Map as Map import Data.Maybe (Maybe(..)) --- | Evaluations driven by the React component lifecycle and by dispatched actions. +-- | Scheduling semantics for an explicit component-scoped task. -- | --- | React may activate, deactivate, and reactivate the same hook instance (notably --- | in development StrictMode), so `Activate` is repeatable. `Update` carries the --- | previous props; current props are available from `Halo.props`. -data Lifecycle props action - = Activate - | Update props - | Action action - --- | How a dispatched action is scheduled. Keyed policies coordinate actions that --- | return the same user-defined key. `Every` is intentionally unkeyed. +-- | `Every` starts every submitted task concurrently and is unkeyed. +-- | `Restartable key` cancels prior running work and discards its queue. +-- | `Drop key` ignores a submission while that key is busy. `Enqueue key` runs +-- | every submission FIFO, one at a time. `KeepLatest key` lets the current task +-- | finish while retaining only the newest queued submission. data TaskPolicy key = Every | Restartable key @@ -36,21 +30,26 @@ data TaskPolicy key | Enqueue key | KeepLatest key --- | The evaluation whose unexpected `Aff` failure reached the error handler. -data ErrorContext props action +-- | Identifies the operation whose unexpected failure reached `onError`. +-- | +-- | `PropsChangeError` carries the previous props. `TaskError` carries the +-- | policy used when the explicit task was submitted. +data ErrorContext props action key = ActivationError | DeactivationError - | UpdateError props + | PropsChangeError props | ActionError action + | TaskError (TaskPolicy key) --- | Running and queued work counts. +-- | Counts of explicit scheduled tasks. Handler and structured-child execution +-- | is intentionally excluded. type TaskCounts = { running :: Int , queued :: Int } --- | A renderable snapshot of scheduler activity. Unkeyed `Every` work appears --- | in the totals but not under a key. +-- | A renderable snapshot of explicit task activity. Unkeyed `Every` tasks +-- | appear in totals but not under a key. newtype Activity key = Activity { total :: TaskCounts , byKey :: Map key TaskCounts @@ -66,15 +65,18 @@ emptyActivity = Activity , byKey: Map.empty } +-- | Read total running and queued explicit task counts. activityTotals :: forall key. Activity key -> TaskCounts activityTotals (Activity activity) = activity.total +-- | Read explicit task counts for one key. activityFor :: forall key. Ord key => key -> Activity key -> TaskCounts activityFor key (Activity activity) = case Map.lookup key activity.byKey of Just counts -> counts Nothing -> { running: 0, queued: 0 } +-- | Identifies a component-scoped emitter subscription. newtype SubscriptionId = SubscriptionId Int derive newtype instance eqSubscriptionId :: Eq SubscriptionId @@ -83,6 +85,7 @@ derive newtype instance ordSubscriptionId :: Ord SubscriptionId derive newtype instance showSubscriptionId :: Show SubscriptionId +-- | Identifies a structured child created with `fork`. newtype ForkId = ForkId Int derive newtype instance eqForkId :: Eq ForkId diff --git a/src/React/Halo/Subscription.purs b/src/React/Halo/Subscription.purs index d27b379..feb24b5 100644 --- a/src/React/Halo/Subscription.purs +++ b/src/React/Halo/Subscription.purs @@ -12,10 +12,13 @@ import Effect (Effect) -- | -- | Registration returns the cleanup effect for that receiver. Halo runs the -- | cleanup when the subscription is removed or its activation scope ends. +-- | Emitters broadcast and do not provide consuming-queue or backpressure +-- | semantics. newtype Emitter action = Emitter ((action -> Effect Unit) -> Effect (Effect Unit)) --- | Create an emitter from registration logic. +-- | Create an emitter from registration logic. A throwing cleanup is isolated +-- | from other scope cleanup and reported as `DeactivationError`. makeEmitter :: forall action . ((action -> Effect Unit) -> Effect (Effect Unit)) diff --git a/test/Main.purs b/test/Main.purs index 5cc94a1..59b3b6f 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -3,7 +3,7 @@ module Test.Main where import Prelude import Effect (Effect) -import Test.Halo.LifecycleSpec as LifecycleSpec +import Test.Halo.ScopeHandlerSpec as ScopeHandlerSpec import Test.Halo.SchedulerSpec as SchedulerSpec import Test.Halo.SubscriptionErrorSpec as SubscriptionErrorSpec import Test.Spec.Reporter (consoleReporter) @@ -12,5 +12,5 @@ import Test.Spec.Runner.Node (runSpecAndExitProcess) main :: Effect Unit main = runSpecAndExitProcess [ consoleReporter ] do SchedulerSpec.spec - LifecycleSpec.spec + ScopeHandlerSpec.spec SubscriptionErrorSpec.spec diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index fb73b8d..c96e4ae 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -31,19 +31,18 @@ derive instance ordTask :: Ord Task loadButton :: Component Props loadButton = Halo.component "LoadButton" { initialState: \_ -> { loading: false, result: Nothing } - , schedule: \Load -> Halo.Restartable GreetingRequest - , eval: case _ of - Halo.Action Load -> do - modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.props - outcome <- liftAff $ attempt loadGreeting - modify_ _ - { loading = false - , result = Just $ case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting - } - _ -> pure unit + , handlers: Halo.defaultHandlers + { onAction = \Load -> Halo.startTask (Halo.Restartable GreetingRequest) do + modify_ _ { loading = true, result = Nothing } + Props { loadGreeting } <- Halo.props + outcome <- liftAff $ attempt loadGreeting + modify_ _ + { loading = false + , result = Just $ case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + } + } , onError: \context error -> Console.error $ "Unexpected Halo failure in " <> showContext context <> ": " <> message error , render: \{ state, dispatch, activity } -> @@ -62,12 +61,13 @@ loadButton = Halo.component "LoadButton" ] } -showContext :: Halo.ErrorContext Props Action -> String +showContext :: Halo.ErrorContext Props Action Task -> String showContext = case _ of Halo.ActivationError -> "activation" Halo.DeactivationError -> "deactivation" - Halo.UpdateError _ -> "props update" - Halo.ActionError Load -> "Load" + Halo.PropsChangeError _ -> "props change" + Halo.ActionError Load -> "Load action" + Halo.TaskError _ -> "greeting task" data WorkflowAction = SearchChanged String @@ -85,21 +85,23 @@ data WorkflowTask derive instance eqWorkflowTask :: Eq WorkflowTask derive instance ordWorkflowTask :: Ord WorkflowTask -workflowSchedule :: WorkflowAction -> Halo.TaskPolicy WorkflowTask -workflowSchedule = case _ of - SearchChanged _ -> Halo.Restartable SearchRequest - SaveClicked -> Halo.Drop SaveRequest - Autosave _ -> Halo.KeepLatest AutosaveRequest - UploadChunk fileId _ -> Halo.Enqueue (Upload fileId) - RecordMetric _ -> Halo.Every +handleWorkflow + :: WorkflowAction + -> Halo.HaloM Unit Unit WorkflowAction WorkflowTask Unit +handleWorkflow = case _ of + SearchChanged _ -> Halo.startTask (Halo.Restartable SearchRequest) (pure unit) + SaveClicked -> Halo.startTask (Halo.Drop SaveRequest) (pure unit) + Autosave _ -> Halo.startTask (Halo.KeepLatest AutosaveRequest) (pure unit) + UploadChunk fileId _ -> Halo.startTask (Halo.Enqueue (Upload fileId)) (pure unit) + RecordMetric _ -> Halo.startTask Halo.Every (pure unit) data SimpleAction = InitializeData simpleEmitter :: Halo.Emitter SimpleAction simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) -simpleEval :: Halo.Lifecycle Unit SimpleAction -> Halo.HaloM Unit Unit SimpleAction Unit Unit -simpleEval = Halo.mkEval $ Halo.defaultEval - { initialize = Just InitializeData - , handleAction = \InitializeData -> pure unit +simpleHandlers :: Halo.Handlers Unit Unit SimpleAction Unit +simpleHandlers = Halo.defaultHandlers + { onActivate = pure unit + , onAction = \InitializeData -> pure unit } diff --git a/test/Test/Halo/Helpers.purs b/test/Test/Halo/Helpers.purs index f029929..cd3f9b5 100644 --- a/test/Test/Halo/Helpers.purs +++ b/test/Test/Halo/Helpers.purs @@ -5,11 +5,11 @@ module Test.Halo.Helpers , Key(..) , await , awaitCounts + , handlers , makeGate , makeHarness - , policyOf , release - , runAction + , runGate , shouldNotHaveStarted , withHarness ) where @@ -26,33 +26,40 @@ import Effect.Aff (Aff, Milliseconds(..)) import Effect.Aff as Aff import Effect.Aff.AVar as AVar import Effect.Aff.Class (liftAff) -import Effect.Class (liftEffect) -import Effect.Exception (message) import Effect.AVar (AVar) import Effect.AVar as EffectAVar +import Effect.Class (liftEffect) +import Effect.Exception (message) import Effect.Ref (Ref) import Effect.Ref as Ref -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate) -import React.Halo.Internal.Types (Activity, ErrorContext(..), Lifecycle(..), TaskCounts, TaskPolicy(..), activityTotals, emptyActivity) +import React.Halo.Handlers (Handlers, defaultHandlers) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, cancelTask, createRuntime, deactivate, fork, startTask) +import React.Halo.Internal.Types (Activity, ErrorContext(..), TaskCounts, TaskPolicy, activityTotals, emptyActivity) import Test.Spec.Assertions (fail, shouldEqual) data Key = Search | Save derive instance eqKey :: Eq Key derive instance ordKey :: Ord Key + instance showKey :: Show Key where show Search = "Search" show Save = "Save" type Gate = - { release :: AVar Unit + { launched :: AVar Unit + , release :: AVar Unit , settled :: AVar Unit , started :: AVar Unit } data Action - = Work (TaskPolicy Key) Int Gate + = StartTask (TaskPolicy Key) Int Gate + | StartTaskWithWitness (TaskPolicy Key) Int Gate Gate + | CancelTask Key (AVar Unit) + | Direct Int Gate | Boom Gate + | TaskBoom (TaskPolicy Key) Gate type Harness = { activity :: Ref (Activity Key) @@ -65,31 +72,56 @@ type Harness = makeGate :: Effect Gate makeGate = do + launched <- EffectAVar.empty started <- EffectAVar.empty releaseGate <- EffectAVar.empty settled <- EffectAVar.empty - pure { started, release: releaseGate, settled } - -policyOf :: Action -> TaskPolicy Key -policyOf = case _ of - Work policy _ _ -> policy - Boom _ -> Every - -runAction :: Lifecycle Unit Action -> HaloM Unit (Array Int) Action Key Unit -runAction = case _ of - Action (Work _ value gate) -> do - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release - modify_ (flip Array.snoc value) - Action (Boom gate) -> - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - (Aff.throwError (Aff.error "boom")) - Activate -> pure unit - Update _ -> pure unit + pure { launched, started, release: releaseGate, settled } + +runGate + :: forall props action key + . Int + -> Gate + -> HaloM props (Array Int) action key Unit +runGate value gate = do + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release + modify_ (flip Array.snoc value) + +handlers :: Handlers Unit (Array Int) Action Key +handlers = defaultHandlers + { onAction = case _ of + StartTask policy value gate -> do + startTask policy (runGate value gate) + liftAff $ void $ AVar.tryPut unit gate.launched + StartTaskWithWitness policy value gate witness -> do + startTask policy (runGate value gate) + void $ fork (runGate 999 witness) + liftAff $ void $ AVar.take witness.started + liftAff $ void $ AVar.tryPut unit gate.launched + CancelTask key completed -> do + cancelTask key + liftAff $ void $ AVar.tryPut unit completed + Direct value gate -> do + liftAff $ void $ AVar.tryPut unit gate.launched + runGate value gate + Boom gate -> do + liftAff $ void $ AVar.tryPut unit gate.launched + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + (Aff.throwError (Aff.error "boom")) + TaskBoom policy gate -> do + startTask policy do + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + Aff.throwError (Aff.error "task boom") + liftAff $ void $ AVar.tryPut unit gate.launched + } makeHarness :: Aff Harness makeHarness = liftEffect do @@ -105,11 +137,10 @@ makeHarness = liftEffect do , initialProps: unit , initialState: [] , spec: - { eval: runAction + { handlers , onError: \context error -> do Ref.modify_ (\current -> Array.snoc current (contextName context <> ": " <> message error)) errors void $ EffectAVar.tryPut unit errorRaised - , schedule: policyOf } , stateUpdate: flip Ref.write state } @@ -147,9 +178,10 @@ awaitCounts harness expected = go 20 void $ await "activity update" harness.activityChanged go (remaining - 1) -contextName :: ErrorContext Unit Action -> String +contextName :: ErrorContext Unit Action Key -> String contextName = case _ of ActivationError -> "activation" DeactivationError -> "deactivation" - UpdateError _ -> "update" + PropsChangeError _ -> "props" ActionError _ -> "action" + TaskError _ -> "task" diff --git a/test/Test/Halo/LifecycleSpec.purs b/test/Test/Halo/LifecycleSpec.purs deleted file mode 100644 index 7c52963..0000000 --- a/test/Test/Halo/LifecycleSpec.purs +++ /dev/null @@ -1,238 +0,0 @@ -module Test.Halo.LifecycleSpec (spec) where - -import Prelude - -import Control.Monad.State (modify_) -import Effect (Effect) -import Effect.Aff as Aff -import Effect.Aff.AVar as AVar -import Effect.Aff.Class (liftAff) -import Effect.AVar (AVar) -import Effect.AVar as EffectAVar -import Effect.Class (liftEffect) -import Effect.Ref as Ref -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, runForTest, syncSpec, updateProps) -import React.Halo.Internal.Types (ErrorContext(..), Lifecycle(..), TaskPolicy(..)) -import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, policyOf, release, shouldNotHaveStarted, withHarness) -import Test.Spec (Spec, describe, it) -import Test.Spec.Assertions (shouldEqual) - -spec :: Spec Unit -spec = describe "scope lifecycle" do - it "cancels running and queued work, clears activity, and fences commits on deactivation" $ withHarness \harness -> do - running <- liftEffect makeGate - queued <- liftEffect makeGate - ignored <- liftEffect makeGate - - liftEffect do - dispatch harness.runtime (Work (Enqueue Save) 1 running) - dispatch harness.runtime (Work (Enqueue Save) 2 queued) - void $ await "running action start before deactivation" running.started - awaitCounts harness { running: 1, queued: 1 } - - liftEffect $ deactivate harness.runtime - void $ await "running action cancellation on deactivation" running.settled - awaitCounts harness { running: 0, queued: 0 } - shouldNotHaveStarted queued - - liftEffect $ dispatch harness.runtime (Work Every 3 ignored) - shouldNotHaveStarted ignored - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [] - - reactivated <- liftEffect makeGate - liftEffect do - activate harness.runtime - dispatch harness.runtime (Work Every 4 reactivated) - void $ await "action start after reactivation" reactivated.started - release reactivated - void $ await "action completion after reactivation" reactivated.settled - awaitCounts harness { running: 0, queued: 0 } - reactivatedState <- liftEffect $ Ref.read harness.state - reactivatedState `shouldEqual` [ 4 ] - - it "models StrictMode setup-cleanup-setup with a fresh usable active scope" do - activation <- liftEffect EffectAVar.empty - state <- liftEffect $ Ref.new 0 - runtime <- liftEffect $ createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit - , initialState: 0 - , spec: - { eval: replayEval activation - , onError: \_ _ -> pure unit - , schedule: \_ -> Every - } - , stateUpdate: flip Ref.write state - } - - Aff.finally (liftEffect $ deactivate runtime) do - liftEffect $ activate runtime - void $ await "first activation" activation - first <- liftEffect $ Ref.read state - first `shouldEqual` 1 - - liftEffect do - deactivate runtime - activate runtime - void $ await "StrictMode replay activation" activation - - pulse <- liftEffect EffectAVar.empty - liftEffect $ dispatch runtime (Pulse pulse) - void $ await "action after StrictMode replay" pulse - second <- liftEffect $ Ref.read state - second `shouldEqual` 12 - - it "owns and cancels prop-update evaluations" do - gate <- liftEffect makeGate - state <- liftEffect $ Ref.new 0 - runtime <- liftEffect $ - ( createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: 0 - , initialState: 0 - , spec: - { eval: case _ of - Update _ -> do - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release - modify_ (_ + 1) - _ -> pure unit - , onError: \_ _ -> pure unit - , schedule: \_ -> Every - } - , stateUpdate: flip Ref.write state - } :: Effect (Runtime Int Int Unit Unit) - ) - - Aff.finally (liftEffect $ deactivate runtime) do - liftEffect do - activate runtime - updateProps runtime 1 - void $ await "props update evaluation start" gate.started - liftEffect $ deactivate runtime - void $ await "props update evaluation cancellation" gate.settled - value <- liftEffect $ Ref.read state - value `shouldEqual` 0 - - it "cancels structured child tasks with their owning evaluation" $ withHarness \harness -> do - childStarted <- liftEffect EffectAVar.empty - childSettled <- liftEffect EffectAVar.empty - parentRelease <- liftEffect EffectAVar.empty - - liftEffect $ runForTest harness.runtime ActivationError do - void $ fork do - liftAff $ Aff.finally - (void $ AVar.tryPut unit childSettled) - do - AVar.put unit childStarted - void $ AVar.take parentRelease - liftAff $ void $ AVar.take parentRelease - - void $ await "structured child start" childStarted - liftEffect $ deactivate harness.runtime - void $ await "structured child cancellation" childSettled - - it "commit-fences structured children when a Restartable parent is replaced" do - firstParent <- liftEffect makeGate - firstChild <- liftEffect makeGate - replacement <- liftEffect makeGate - replacementDone <- liftEffect EffectAVar.empty - state <- liftEffect $ Ref.new 0 - runtime <- liftEffect $ createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit - , initialState: 0 - , spec: - { eval: childEval - , onError: \_ _ -> pure unit - , schedule: \_ -> Restartable unit - } - , stateUpdate: flip Ref.write state - } - - Aff.finally (liftEffect $ deactivate runtime) do - liftEffect do - activate runtime - dispatch runtime (ParentWithChild firstParent firstChild) - void $ await "Restartable parent start" firstParent.started - void $ await "Restartable child start" firstChild.started - - liftEffect $ dispatch runtime (Replacement replacement replacementDone) - void $ await "replaced parent cancellation" firstParent.settled - void $ await "replaced child cancellation" firstChild.settled - void $ await "replacement parent start" replacement.started - release replacement - void $ await "replacement parent state commit" replacementDone - - value <- liftEffect $ Ref.read state - value `shouldEqual` 10 - - it "uses the latest evaluator and handlers after the hook spec changes" $ withHarness \harness -> do - gate <- liftEffect makeGate - liftEffect do - syncSpec harness.runtime - { activityUpdate: \next -> do - Ref.write next harness.activity - void $ EffectAVar.tryPut unit harness.activityChanged - , spec: - { eval: case _ of - Action (Work _ value workGate) -> do - liftAff do - AVar.put unit workGate.started - void $ AVar.take workGate.release - modify_ (flip append [ value * 10 ]) - _ -> pure unit - , onError: \_ _ -> pure unit - , schedule: policyOf - } - , stateUpdate: flip Ref.write harness.state - } - dispatch harness.runtime (Work Every 2 gate) - - void $ await "action using replacement evaluator" gate.started - release gate - awaitCounts harness { running: 0, queued: 0 } - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 20 ] - -data ChildAction - = ParentWithChild Gate Gate - | Replacement Gate (AVar Unit) - -childEval :: Lifecycle Unit ChildAction -> HaloM Unit Int ChildAction Unit Unit -childEval = case _ of - Activate -> pure unit - Update _ -> pure unit - Action (ParentWithChild parent child) -> do - void $ fork do - runGate child - modify_ (_ + 100) - runGate parent - modify_ (_ + 1) - Action (Replacement gate completed) -> do - runGate gate - modify_ (_ + 10) - liftAff $ void $ AVar.tryPut unit completed - -runGate :: forall props state action key. Gate -> HaloM props state action key Unit -runGate gate = liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release - -data ReplayAction = Pulse (AVar Unit) - -replayEval :: AVar Unit -> Lifecycle Unit ReplayAction -> HaloM Unit Int ReplayAction Unit Unit -replayEval activation = case _ of - Activate -> do - modify_ (_ + 1) - liftAff $ void $ AVar.tryPut unit activation - Action (Pulse completed) -> do - modify_ (_ + 10) - liftAff $ void $ AVar.tryPut unit completed - Update _ -> pure unit diff --git a/test/Test/Halo/SchedulerSpec.purs b/test/Test/Halo/SchedulerSpec.purs index febfe57..daf25e3 100644 --- a/test/Test/Halo/SchedulerSpec.purs +++ b/test/Test/Halo/SchedulerSpec.purs @@ -3,6 +3,7 @@ module Test.Halo.SchedulerSpec (spec) where import Prelude import Data.Array as Array +import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Ref as Ref import React.Halo.Internal.Runtime (dispatch) @@ -12,126 +13,179 @@ import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) spec :: Spec Unit -spec = describe "action scheduling" do - it "Every runs every action concurrently" $ withHarness \harness -> do +spec = describe "explicit task scheduling" do + it "handles an action immediately without counting it as task activity" $ withHarness \harness -> do + gate <- liftEffect makeGate + liftEffect $ dispatch harness.runtime (Direct 1 gate) + void $ await "direct action start" gate.started + + awaitCounts harness { running: 0, queued: 0 } + release gate + void $ await "direct action completion" gate.settled + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 1 ] + + it "keeps a submitted task alive after its launching action completes" $ withHarness \harness -> do + gate <- liftEffect makeGate + witness <- liftEffect makeGate + liftEffect $ dispatch harness.runtime (StartTaskWithWitness Every 1 gate witness) + + void $ await "task submission" gate.launched + void $ await "launching handler completion" witness.settled + void $ await "submitted task start" gate.started + awaitCounts harness { running: 1, queued: 0 } + + release gate + void $ await "submitted task completion" gate.settled + awaitCounts harness { running: 0, queued: 0 } + + it "Every runs every submitted task concurrently" $ withHarness \harness -> do first <- liftEffect makeGate second <- liftEffect makeGate liftEffect do - dispatch harness.runtime (Work Every 1 first) - dispatch harness.runtime (Work Every 2 second) + dispatch harness.runtime (StartTask Every 1 first) + dispatch harness.runtime (StartTask Every 2 second) - void $ await "first Every action start" first.started - void $ await "second Every action start" second.started + void $ await "first Every task launch" first.launched + void $ await "second Every task launch" second.launched + void $ await "first Every task start" first.started + void $ await "second Every task start" second.started awaitCounts harness { running: 2, queued: 0 } release second release first - void $ await "first Every action completion" first.settled - void $ await "second Every action completion" second.settled + void $ await "first Every task completion" first.settled + void $ await "second Every task completion" second.settled awaitCounts harness { running: 0, queued: 0 } state <- liftEffect $ Ref.read harness.state Array.sort state `shouldEqual` [ 1, 2 ] - it "Restartable cancels and commit-fences the previous keyed action" $ withHarness \harness -> do + it "Restartable cancels and commit-fences the previous keyed task" $ withHarness \harness -> do stale <- liftEffect makeGate current <- liftEffect makeGate - liftEffect $ dispatch harness.runtime (Work (Restartable Search) 1 stale) - void $ await "stale Restartable action start" stale.started - liftEffect $ dispatch harness.runtime (Work (Restartable Search) 2 current) + liftEffect $ dispatch harness.runtime (StartTask (Restartable Search) 1 stale) + void $ await "stale Restartable task start" stale.started + liftEffect $ dispatch harness.runtime (StartTask (Restartable Search) 2 current) - void $ await "stale Restartable action cancellation" stale.settled - void $ await "replacement Restartable action start" current.started + void $ await "stale Restartable task cancellation" stale.settled + void $ await "replacement Restartable task start" current.started awaitCounts harness { running: 1, queued: 0 } release current - void $ await "replacement Restartable action completion" current.settled + void $ await "replacement Restartable task completion" current.settled awaitCounts harness { running: 0, queued: 0 } state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 2 ] - it "Drop ignores new work while the key is running" $ withHarness \harness -> do + it "Drop ignores a new task while the key is busy" $ withHarness \harness -> do running <- liftEffect makeGate dropped <- liftEffect makeGate - liftEffect $ dispatch harness.runtime (Work (Drop Save) 1 running) - void $ await "Drop action start" running.started - liftEffect $ dispatch harness.runtime (Work (Drop Save) 2 dropped) + liftEffect $ dispatch harness.runtime (StartTask (Drop Save) 1 running) + void $ await "Drop task start" running.started + liftEffect $ dispatch harness.runtime (StartTask (Drop Save) 2 dropped) + void $ await "dropped task launching action completion" dropped.launched awaitCounts harness { running: 1, queued: 0 } shouldNotHaveStarted dropped release running - void $ await "Drop action completion" running.settled + void $ await "Drop task completion" running.settled awaitCounts harness { running: 0, queued: 0 } shouldNotHaveStarted dropped state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1 ] - it "Enqueue runs all keyed actions FIFO, one at a time" $ withHarness \harness -> do + it "Enqueue runs all keyed tasks FIFO, one at a time" $ withHarness \harness -> do first <- liftEffect makeGate second <- liftEffect makeGate third <- liftEffect makeGate liftEffect do - dispatch harness.runtime (Work (Enqueue Save) 1 first) - dispatch harness.runtime (Work (Enqueue Save) 2 second) - dispatch harness.runtime (Work (Enqueue Save) 3 third) + dispatch harness.runtime (StartTask (Enqueue Save) 1 first) + dispatch harness.runtime (StartTask (Enqueue Save) 2 second) + dispatch harness.runtime (StartTask (Enqueue Save) 3 third) - void $ await "first Enqueue action start" first.started + void $ await "first Enqueue task start" first.started + void $ await "second Enqueue task submission" second.launched + void $ await "third Enqueue task submission" third.launched awaitCounts harness { running: 1, queued: 2 } shouldNotHaveStarted second shouldNotHaveStarted third release first - void $ await "second Enqueue action start" second.started + void $ await "second Enqueue task start" second.started awaitCounts harness { running: 1, queued: 1 } release second - void $ await "third Enqueue action start" third.started + void $ await "third Enqueue task start" third.started awaitCounts harness { running: 1, queued: 0 } release third - void $ await "third Enqueue action completion" third.settled + void $ await "third Enqueue task completion" third.settled awaitCounts harness { running: 0, queued: 0 } state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1, 2, 3 ] - it "KeepLatest finishes current work and retains only the newest queued action" $ withHarness \harness -> do + it "KeepLatest retains only the newest queued task" $ withHarness \harness -> do first <- liftEffect makeGate discarded <- liftEffect makeGate latest <- liftEffect makeGate liftEffect do - dispatch harness.runtime (Work (KeepLatest Search) 1 first) - dispatch harness.runtime (Work (KeepLatest Search) 2 discarded) - dispatch harness.runtime (Work (KeepLatest Search) 3 latest) + dispatch harness.runtime (StartTask (KeepLatest Search) 1 first) + dispatch harness.runtime (StartTask (KeepLatest Search) 2 discarded) + dispatch harness.runtime (StartTask (KeepLatest Search) 3 latest) - void $ await "current KeepLatest action start" first.started + void $ await "current KeepLatest task start" first.started + void $ await "discarded task submission" discarded.launched + void $ await "latest task submission" latest.launched awaitCounts harness { running: 1, queued: 1 } shouldNotHaveStarted discarded shouldNotHaveStarted latest release first - void $ await "latest KeepLatest action start" latest.started + void $ await "latest KeepLatest task start" latest.started shouldNotHaveStarted discarded release latest - void $ await "latest KeepLatest action completion" latest.settled + void $ await "latest KeepLatest task completion" latest.settled awaitCounts harness { running: 0, queued: 0 } state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1, 3 ] - it "reports keyed running and queued activity for rendering" $ withHarness \harness -> do + it "cancels running and queued tasks for a key" $ withHarness \harness -> do + running <- liftEffect makeGate + queued <- liftEffect makeGate + cancelled <- liftEffect EffectAVar.empty + + liftEffect do + dispatch harness.runtime (StartTask (Enqueue Search) 1 running) + dispatch harness.runtime (StartTask (Enqueue Search) 2 queued) + void $ await "task before keyed cancellation" running.started + void $ await "queued task submission" queued.launched + awaitCounts harness { running: 1, queued: 1 } + + liftEffect $ dispatch harness.runtime (CancelTask Search cancelled) + void $ await "keyed cancellation action" cancelled + void $ await "running keyed task cancellation" running.settled + awaitCounts harness { running: 0, queued: 0 } + shouldNotHaveStarted queued + + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [] + + it "reports explicit keyed task activity for rendering" $ withHarness \harness -> do running <- liftEffect makeGate queued <- liftEffect makeGate liftEffect do - dispatch harness.runtime (Work (Enqueue Search) 1 running) - dispatch harness.runtime (Work (Enqueue Search) 2 queued) - void $ await "keyed activity action start" running.started + dispatch harness.runtime (StartTask (Enqueue Search) 1 running) + dispatch harness.runtime (StartTask (Enqueue Search) 2 queued) + void $ await "keyed activity task start" running.started awaitCounts harness { running: 1, queued: 1 } activity <- liftEffect $ Ref.read harness.activity @@ -139,7 +193,7 @@ spec = describe "action scheduling" do activityFor Save activity `shouldEqual` { running: 0, queued: 0 } release running - void $ await "queued keyed activity action start" queued.started + void $ await "queued keyed activity task start" queued.started release queued - void $ await "queued keyed activity action completion" queued.settled + void $ await "queued keyed activity task completion" queued.settled awaitCounts harness { running: 0, queued: 0 } diff --git a/test/Test/Halo/ScopeHandlerSpec.purs b/test/Test/Halo/ScopeHandlerSpec.purs new file mode 100644 index 0000000..209fab3 --- /dev/null +++ b/test/Test/Halo/ScopeHandlerSpec.purs @@ -0,0 +1,277 @@ +module Test.Halo.ScopeHandlerSpec (spec) where + +import Prelude + +import Control.Monad.State (modify_) +import Data.Tuple (Tuple(..)) +import Effect (Effect) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.AVar (AVar) +import Effect.AVar as EffectAVar +import Effect.Class (liftEffect) +import Effect.Ref as Ref +import React.Halo.Handlers (defaultHandlers) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, props, startTask, syncSpec, updateProps) +import React.Halo.Internal.Types (TaskPolicy(..)) +import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec :: Spec Unit +spec = describe "scope and handlers" do + it "cancels explicit tasks on deactivation and accepts work after reactivation" $ withHarness \harness -> do + running <- liftEffect makeGate + queued <- liftEffect makeGate + ignored <- liftEffect makeGate + + liftEffect do + dispatch harness.runtime (StartTask (Enqueue Save) 1 running) + dispatch harness.runtime (StartTask (Enqueue Save) 2 queued) + void $ await "running task before deactivation" running.started + awaitCounts harness { running: 1, queued: 1 } + + liftEffect $ deactivate harness.runtime + void $ await "running task cancellation on deactivation" running.settled + awaitCounts harness { running: 0, queued: 0 } + shouldNotHaveStarted queued + + liftEffect $ dispatch harness.runtime (StartTask Every 3 ignored) + shouldNotHaveStarted ignored + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [] + + reactivated <- liftEffect makeGate + liftEffect do + activate harness.runtime + dispatch harness.runtime (StartTask Every 4 reactivated) + void $ await "task start after reactivation" reactivated.started + release reactivated + void $ await "task completion after reactivation" reactivated.settled + awaitCounts harness { running: 0, queued: 0 } + reactivatedState <- liftEffect $ Ref.read harness.state + reactivatedState `shouldEqual` [ 4 ] + + it "models StrictMode setup-cleanup-setup with repeatable onActivate" do + activation <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ + ( createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: 0 + , spec: + { handlers: defaultHandlers + { onActivate = do + modify_ (_ + 1) + liftAff $ void $ AVar.tryPut unit activation + , onAction = \(Pulse completed) -> do + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit completed + } + , onError: \_ _ -> pure unit + } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Unit Int ReplayAction Unit) + ) + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect $ activate runtime + void $ await "first activation" activation + first <- liftEffect $ Ref.read state + first `shouldEqual` 1 + + liftEffect do + deactivate runtime + activate runtime + void $ await "StrictMode replay activation" activation + + pulse <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (Pulse pulse) + void $ await "action after StrictMode replay" pulse + second <- liftEffect $ Ref.read state + second `shouldEqual` 12 + + it "passes previous props and exposes current props to onPropsChange" do + changed <- liftEffect EffectAVar.empty + runtime <- liftEffect $ + ( createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: 0 + , initialState: unit + , spec: + { handlers: defaultHandlers + { onPropsChange = \previous -> do + current <- props + liftAff $ void $ AVar.tryPut (Tuple previous current) changed + } + , onError: \_ _ -> pure unit + } + , stateUpdate: \_ -> pure unit + } :: Effect (Runtime Int Unit Unit Unit) + ) + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + updateProps runtime 1 + values <- await "props change handler" changed + values `shouldEqual` Tuple 0 1 + + it "owns and cancels a running props-change handler" do + gate <- liftEffect makeGate + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ + ( createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: 0 + , initialState: 0 + , spec: + { handlers: defaultHandlers + { onPropsChange = \_ -> do + runIntGate gate + modify_ (_ + 1) + } + , onError: \_ _ -> pure unit + } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Int Int Unit Unit) + ) + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + updateProps runtime 1 + void $ await "props-change handler start" gate.started + liftEffect $ deactivate runtime + void $ await "props-change handler cancellation" gate.settled + value <- liftEffect $ Ref.read state + value `shouldEqual` 0 + + it "cancels a structured fork when its action handler finishes" do + child <- liftEffect makeGate + handlerDone <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ + ( createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: 0 + , spec: + { handlers: defaultHandlers + { onAction = \(ForkAndReturn gate completed) -> do + void $ fork do + runIntGate gate + modify_ (_ + 100) + liftAff $ void $ AVar.take gate.started + liftAff $ void $ AVar.tryPut unit completed + } + , onError: \_ _ -> pure unit + } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Unit Int ForkAction Unit) + ) + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (ForkAndReturn child handlerDone) + void $ await "action handler return" handlerDone + void $ await "structured child cancellation" child.settled + value <- liftEffect $ Ref.read state + value `shouldEqual` 0 + + it "commit-fences a task and its structured child when replaced" do + firstParent <- liftEffect makeGate + firstChild <- liftEffect makeGate + replacement <- liftEffect makeGate + replacementDone <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ createRuntime + { activityUpdate: \_ -> pure unit + , initialProps: unit + , initialState: 0 + , spec: + { handlers: defaultHandlers + { onAction = case _ of + ParentTask parent child -> startTask (Restartable unit) do + void $ fork do + runIntGate child + modify_ (_ + 100) + liftAff $ void $ AVar.take child.started + runIntGate parent + modify_ (_ + 1) + ReplacementTask gate completed -> startTask (Restartable unit) do + runIntGate gate + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit completed + } + , onError: \_ _ -> pure unit + } + , stateUpdate: flip Ref.write state + } + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (ParentTask firstParent firstChild) + void $ await "parent task start" firstParent.started + + liftEffect $ dispatch runtime (ReplacementTask replacement replacementDone) + void $ await "replaced parent task cancellation" firstParent.settled + void $ await "replaced structured child cancellation" firstChild.settled + void $ await "replacement task start" replacement.started + release replacement + void $ await "replacement task completion" replacementDone + + value <- liftEffect $ Ref.read state + value `shouldEqual` 10 + + it "uses the latest handlers after the hook spec changes" $ withHarness \harness -> do + gate <- liftEffect makeGate + liftEffect do + syncSpec harness.runtime + { activityUpdate: \next -> do + Ref.write next harness.activity + void $ EffectAVar.tryPut unit harness.activityChanged + , spec: + { handlers: defaultHandlers + { onAction = case _ of + Direct value workGate -> do + liftAff $ void $ AVar.tryPut unit workGate.launched + liftAff do + AVar.put unit workGate.started + void $ AVar.take workGate.release + modify_ (flip append [ value * 10 ]) + liftAff $ void $ AVar.tryPut unit workGate.settled + _ -> pure unit + } + , onError: \_ _ -> pure unit + } + , stateUpdate: flip Ref.write harness.state + } + dispatch harness.runtime (Direct 2 gate) + + void $ await "action using replacement handler" gate.started + release gate + void $ await "replacement handler completion" gate.settled + awaitCounts harness { running: 0, queued: 0 } + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 20 ] + +data ReplayAction = Pulse (AVar Unit) + +data ForkAction = ForkAndReturn Gate (AVar Unit) + +data ParentAction + = ParentTask Gate Gate + | ReplacementTask Gate (AVar Unit) + +runIntGate :: forall props action key. Gate -> HaloM props Int action key Unit +runIntGate gate = do + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index 1e2f016..1502f3e 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -13,36 +13,28 @@ import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Exception as Exception import Effect.Ref as Ref -import React.Halo.Internal.Runtime (HaloM, activate, createRuntime, deactivate, dispatch, subscribe, unsubscribe) -import React.Halo.Internal.Types (ErrorContext(..), Lifecycle(..), SubscriptionId, TaskPolicy(..), activityTotals, emptyActivity) +import React.Halo.Handlers (Handlers, defaultHandlers) +import React.Halo.Internal.Runtime (activate, createRuntime, deactivate, dispatch, subscribe, syncSpec, unsubscribe) +import React.Halo.Internal.Types (ErrorContext(..), SubscriptionId, TaskPolicy(..)) import React.Halo.Subscription (Emitter, makeEmitter) -import Test.Halo.Helpers (Action(..), Gate, await, awaitCounts, makeGate, withHarness) +import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, handlers, makeGate, withHarness) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) spec :: Spec Unit spec = describe "subscriptions and errors" do - it "removes a manually unsubscribed resource from component tracking" do + it "removes a manual unsubscribe from scope tracking" do cleanupCount <- liftEffect $ Ref.new 0 - callback <- liftEffect $ Ref.new Nothing started <- liftEffect EffectAVar.empty stopped <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new Nothing - activity <- liftEffect $ Ref.new emptyActivity - let - emitter = makeEmitter \receive -> do - Ref.write (Just receive) callback - pure $ Ref.modify_ (_ + 1) cleanupCount + let emitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleanupCount runtime <- liftEffect $ createRuntime - { activityUpdate: flip Ref.write activity + { activityUpdate: \_ -> pure unit , initialProps: unit , initialState: Nothing - , spec: - { eval: subscriptionEval - , onError: \_ _ -> pure unit - , schedule: \_ -> Every - } + , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state } @@ -56,20 +48,11 @@ spec = describe "subscriptions and errors" do afterManual <- liftEffect $ Ref.read cleanupCount afterManual `shouldEqual` 1 - liftEffect do - deactivate runtime - activate runtime + liftEffect $ deactivate runtime afterDeactivation <- liftEffect $ Ref.read cleanupCount afterDeactivation `shouldEqual` 1 - -- Even if a broken source invokes its retained callback after cleanup, - -- that callback is bound to the old scope and cannot target reactivation. - retained <- liftEffect $ Ref.read callback - liftEffect $ traverse_ (_ $ Ping) retained - counts <- activityTotals <$> liftEffect (Ref.read activity) - counts `shouldEqual` { running: 0, queued: 0 } - - it "unsubscribes tracked resources on deactivation" do + it "runs tracked subscription cleanup on deactivation" do cleanupCount <- liftEffect $ Ref.new 0 started <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new Nothing @@ -79,11 +62,7 @@ spec = describe "subscriptions and errors" do { activityUpdate: \_ -> pure unit , initialProps: unit , initialState: Nothing - , spec: - { eval: subscriptionEval - , onError: \_ _ -> pure unit - , schedule: \_ -> Every - } + , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state } @@ -96,7 +75,7 @@ spec = describe "subscriptions and errors" do cleaned <- liftEffect $ Ref.read cleanupCount cleaned `shouldEqual` 1 - it "continues deactivation when a subscription cleanup throws" do + it "isolates throwing cleanup and reports DeactivationError" do cleaned <- liftEffect $ Ref.new 0 cleanupErrors <- liftEffect $ Ref.new [] badStarted <- liftEffect EffectAVar.empty @@ -112,11 +91,10 @@ spec = describe "subscriptions and errors" do , initialProps: unit , initialState: Nothing , spec: - { eval: subscriptionEval + { handlers: subscriptionHandlers , onError: \context error -> case context of DeactivationError -> Ref.modify_ (_ <> [ Exception.message error ]) cleanupErrors _ -> Ref.modify_ (_ <> [ "wrong error context" ]) cleanupErrors - , schedule: \_ -> Every } , stateUpdate: flip Ref.write state } @@ -128,7 +106,7 @@ spec = describe "subscriptions and errors" do liftEffect $ dispatch runtime (Start goodEmitter goodStarted) void $ await "successful subscription setup" goodStarted liftEffect $ dispatch runtime (Block gate) - void $ await "running action" gate.started + void $ await "running action handler" gate.started liftEffect $ deactivate runtime void $ await "running action cancellation" gate.settled @@ -138,40 +116,76 @@ spec = describe "subscriptions and errors" do errors <- liftEffect $ Ref.read cleanupErrors errors `shouldEqual` [ "cleanup failed" ] - it "routes unexpected action failures with action context" $ withHarness \harness -> do + it "routes an unexpected action failure with ActionError" $ withHarness \harness -> do gate <- liftEffect makeGate liftEffect $ dispatch harness.runtime (Boom gate) - void $ await "spec-level error handler" harness.errorRaised - awaitCounts harness { running: 0, queued: 0 } + void $ await "action error handler" harness.errorRaised errors <- liftEffect $ Ref.read harness.errors errors `shouldEqual` [ "action: boom" ] + it "uses the latest unexpected-error callback after a spec change" $ withHarness \harness -> do + replacementErrors <- liftEffect $ Ref.new [] + replacementRaised <- liftEffect EffectAVar.empty + gate <- liftEffect makeGate + liftEffect do + syncSpec harness.runtime + { activityUpdate: \next -> do + Ref.write next harness.activity + void $ EffectAVar.tryPut unit harness.activityChanged + , spec: + { handlers + , onError: \context error -> do + let + label = case context of + ActionError _ -> "replacement action" + _ -> "wrong replacement context" + Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) replacementErrors + void $ EffectAVar.tryPut unit replacementRaised + } + , stateUpdate: flip Ref.write harness.state + } + dispatch harness.runtime (Boom gate) + + void $ await "replacement error callback" replacementRaised + oldErrors <- liftEffect $ Ref.read harness.errors + oldErrors `shouldEqual` [] + newErrors <- liftEffect $ Ref.read replacementErrors + newErrors `shouldEqual` [ "replacement action: boom" ] + + it "routes an explicit task failure with TaskError" $ withHarness \harness -> do + gate <- liftEffect makeGate + liftEffect $ dispatch harness.runtime (TaskBoom (Restartable Save) gate) + void $ await "failing task start" gate.started + void $ await "task error handler" harness.errorRaised + awaitCounts harness { running: 0, queued: 0 } + + errors <- liftEffect $ Ref.read harness.errors + errors `shouldEqual` [ "task: task boom" ] + data SubscriptionAction = Start (Emitter SubscriptionAction) (AVar Unit) | Stop (AVar Unit) | Block Gate - | Ping - -subscriptionEval - :: Lifecycle Unit SubscriptionAction - -> HaloM Unit (Maybe SubscriptionId) SubscriptionAction Unit Unit -subscriptionEval = case _ of - Activate -> pure unit - Update _ -> pure unit - Action (Start emitter completed) -> do - sid <- subscribe emitter - put (Just sid) - liftAff $ void $ AVar.tryPut unit completed - Action (Stop completed) -> do - sid <- get - traverse_ unsubscribe sid - put Nothing - liftAff $ void $ AVar.tryPut unit completed - Action (Block gate) -> - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release - Action Ping -> pure unit + +type SubscriptionState = Maybe SubscriptionId + +subscriptionHandlers :: Handlers Unit SubscriptionState SubscriptionAction Unit +subscriptionHandlers = defaultHandlers + { onAction = case _ of + Start emitter completed -> do + sid <- subscribe emitter + put (Just sid) + liftAff $ void $ AVar.tryPut unit completed + Stop completed -> do + sid <- get + traverse_ unsubscribe sid + put Nothing + liftAff $ void $ AVar.tryPut unit completed + Block gate -> + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release + } From cc628cdce1c91b76ba9be05f0d72af294c586b19 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 18:16:11 +0900 Subject: [PATCH 04/16] Define first-class Halo tasks --- README.md | 47 +++-- docs/guide.md | 243 +++++++++------------- docs/migration-v4.md | 104 +++++---- docs/reference.md | 127 ++++++----- src/React/Halo.purs | 5 +- src/React/Halo/Internal/Runtime.purs | 124 ++++++----- src/React/Halo/Internal/Task.purs | 41 ++++ src/React/Halo/Internal/Types.purs | 35 +--- src/React/Halo/Task.purs | 106 ++++++++++ test/Test/Halo/DocExamples.purs | 65 ++++-- test/Test/Halo/Helpers.purs | 57 +++-- test/Test/Halo/SchedulerSpec.purs | 210 +++++++++++-------- test/Test/Halo/ScopeHandlerSpec.purs | 47 +++-- test/Test/Halo/SubscriptionErrorSpec.purs | 16 +- 14 files changed, 734 insertions(+), 493 deletions(-) create mode 100644 src/React/Halo/Internal/Task.purs create mode 100644 src/React/Halo/Task.purs diff --git a/README.md b/README.md index 90150a6..6a46939 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # React Halo -Halo gives a PureScript React component one typed action handler plus explicit, component-scoped asynchronous tasks. It is for event-driven UI workflows where plain hooks become hard to coordinate: replace stale searches, prevent overlapping saves, preserve upload order, or retain only the newest pending refresh. +Halo gives a PureScript React component one typed action handler plus reusable, component-scoped tasks. It is for UI workflows where plain hooks become hard to coordinate: replace stale searches, prevent overlapping saves, preserve upload order, or retain only the newest pending refresh. For one request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. Halo earns its place when actions, state transitions, cancellation, and task concurrency need one coherent owner. @@ -8,11 +8,11 @@ For one request derived directly from render dependencies, `React.Basic.Hooks.Af Halo separates three kinds of work: -1. **Handlers** react to activation, prop changes, and dispatched actions. They start immediately, are owned by the active React scope, and do not count as task activity. -2. **Tasks** are submitted explicitly with `startTask`. They can outlive the handler that submitted them, use a named concurrency policy, drive `Activity`, and are cancelled on deactivation. +1. **Handlers** react to activation, prop changes, and dispatched actions. They start immediately, belong to the active React scope, and do not count as task activity. +2. **Tasks** are first-class definitions created with `concurrent`, `restartable`, `drop`, `enqueue`, or `keepLatest`. A definition binds its identity, scheduling strategy, and input-driven implementation. `perform` submits work that can outlive its caller and drives `Activity`. 3. **Structured children** are created with `fork`. A child belongs to its current handler or task and is cancelled when that parent finishes. -An action is an event, not an implicit task. The action handler decides whether to update state immediately, start a task, cancel keyed tasks, subscribe to events, or combine those operations. +An action is an event, not an implicit task. The action handler decides whether to update state, perform or cancel a task, subscribe to events, or combine those operations. ## Try the unreleased v4 @@ -40,7 +40,7 @@ Your application also needs the JavaScript packages required by `react-basic-hoo ## Quick start: replace a stale request -This component handles every click immediately, then explicitly submits a restartable request. A second click fences and cancels the prior `GreetingRequest` task before starting another. +Define the request once as a restartable task. Each click dispatches an action immediately; `perform_` then fences and cancels prior work for `GreetingRequest` before starting the new request. ```purescript module Example.LoadButton where @@ -68,31 +68,32 @@ type State = data Action = Load -data Task = GreetingRequest +data TaskKey = GreetingRequest -derive instance eqTask :: Eq Task -derive instance ordTask :: Ord Task +derive instance eqTaskKey :: Eq TaskKey +derive instance ordTaskKey :: Ord TaskKey + +loadGreetingTask :: Halo.Task Props State Action TaskKey Unit +loadGreetingTask = Halo.restartable GreetingRequest \_ -> do + modify_ _ { loading = true, result = Nothing } + Props { loadGreeting } <- Halo.props + outcome <- liftAff $ attempt loadGreeting + modify_ _ + { loading = false + , result = Just $ case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + } loadButton :: Component Props loadButton = Halo.component "LoadButton" { initialState: \_ -> { loading: false, result: Nothing } , handlers: Halo.defaultHandlers - { onAction = \Load -> - Halo.startTask (Halo.Restartable GreetingRequest) do - modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.props - outcome <- liftAff $ attempt loadGreeting - modify_ _ - { loading = false - , result = Just $ case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting - } - } + { onAction = \Load -> Halo.perform_ loadGreetingTask } , onError: \_ error -> Console.error $ "Unexpected Halo failure: " <> message error , render: \{ state, dispatch, activity } -> - let counts = Halo.activityFor GreetingRequest activity + let counts = Halo.activity loadGreetingTask activity in R.div_ [ R.button { onClick: capture_ (dispatch Load) @@ -107,11 +108,11 @@ loadButton = Halo.component "LoadButton" } ``` -The request catches an expected domain failure and stores it in state. Unexpected failures that escape a handler or task go to `onError` with an `ErrorContext`. +The task catches an expected domain failure and stores it in state. Unexpected failures that escape a handler or task go to `onError` with an `ErrorContext`. ## Learn and reference -- [Guide](docs/guide.md): handlers, explicit tasks, policies, cancellation, activity, subscriptions, lifecycle, patterns, and troubleshooting. +- [Guide](docs/guide.md): handlers, task definitions, scheduling, cancellation, activity, subscriptions, activation, patterns, and troubleshooting. - [API reference](docs/reference.md): public types and operations with exact semantics. - [v3 to v4 migration](docs/migration-v4.md): breaking changes and a practical conversion sequence. diff --git a/docs/guide.md b/docs/guide.md index 7a0899b..a85176f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,53 +1,57 @@ # Halo v4 guide -This guide explains how to design a component around Halo's explicit action and task model. Start with the [README quick start](../README.md) if you have not built a Halo component yet. Exact signatures are in the [API reference](reference.md). +This guide explains Halo's action and first-class task model. Start with the [README quick start](../README.md); use the [API reference](reference.md) for exact signatures. ## Choose Halo for coordinated component workflows -React hooks remain the default. Prefer `useAff` when one asynchronous result follows render dependencies and latest-request cancellation is the only coordination you need. - -Halo is useful when a component has a small event protocol and several operations must share state and cancellation rules. Typical examples include: - -- search input where each request replaces the previous request; -- a save button that ignores duplicate clicks; -- per-file upload chunks that must preserve order; -- autosave where the current write may finish but only the newest pending value matters; and -- event sources that dispatch into the same state owner. +Prefer ordinary React hooks when one asynchronous result follows render dependencies. Halo is useful when a component has an event protocol and several operations must share state and cancellation rules—for example, latest-request-wins search, duplicate-save prevention, ordered per-file uploads, or autosave that retains only the newest pending value. Halo does not provide global state, server caching, a process/saga runtime, or server-side rendering machinery. -## Think in actions, tasks, and structured children - -### Actions describe events +## Think in handlers, tasks, and structured children -Rendering code calls `dispatch :: action -> Effect Unit`. Halo starts `handlers.onAction action` in the active component scope. Every dispatched action gets a handler execution; actions are not deduplicated, queued, or assigned task policies. +### Handlers respond to events -Use an action handler for quick state transitions and decisions: +Rendering code calls `dispatch :: action -> Effect Unit`. Halo immediately starts `handlers.onAction action` in the active component scope. Actions are not queued, deduplicated, or automatically treated as tasks. ```purescript onAction = case _ of NameChanged name -> modify_ _ { name = name } - CancelSearch -> Halo.cancelTask SearchRequest - SearchSubmitted query -> - Halo.startTask (Halo.Restartable SearchRequest) (search query) + SearchSubmitted query -> Halo.perform searchTask query + SearchCleared -> Halo.cancel searchTask +``` + +Handlers are scope-owned and commit-fenced after deactivation, but their execution is not shown in `Activity`. + +### Task definitions bind identity, strategy, and work + +A task is a reusable value: + +```purescript +searchTask = Halo.restartable SearchRequest \query -> do + results <- liftAff $ fetchResults query + modify_ _ { query = query, results = results } ``` -Handler execution is scope-owned and commit-fenced after deactivation, but it is not shown in `Activity`. +Its type is `Task props state action key input`. The `key` identifies the scheduler slot; `input` is supplied separately on each `perform`. The smart constructor fixes the scheduling strategy so a call site cannot accidentally change concurrency behavior. -### Explicit tasks describe asynchronous work +`perform task input` submits work and returns immediately. `perform_ task` is the `Unit`-input convenience form. Submitted work belongs to the active component scope, not to the handler or task that submitted it, so it can outlive successful caller completion. It may read props, update state, create structured children, or perform another task. -`startTask policy computation` submits `computation` to the component task scheduler and returns immediately. The submitted task belongs to the active component scope, not to the handler that submitted it. It can therefore keep running after a successful action handler returns. +Define stable tasks near the workflow they implement and perform them from handlers. A function may return a task when the key itself is dynamic: -A task may read current props, update Halo state, create structured children, or submit another component-scoped task. Its state commits are fenced when it is replaced or cancelled. +```purescript +uploadTask fileId = Halo.enqueue (Upload fileId) \chunk -> upload chunk -Make task submission visible where the action is handled. Do not hide it behind a second action-to-policy table; the policy belongs next to the work whose concurrency it controls. +onAction (UploadChunk fileId chunk) = + Halo.perform (uploadTask fileId) chunk +``` -### Structured children describe parent-bound concurrency +### Structured children stay with their parent -`fork child` starts `child` concurrently under the current handler or task. Unlike an explicit task, a forked child is cancelled when its parent finishes normally. Use `fork` when the parent remains alive and owns concurrent subwork. Use `startTask` when work must outlive the action handler that launched it. +`fork child` starts concurrent work owned by the current handler or task. Unlike performed work, a forked child is cancelled when its parent finishes normally. ```purescript -Halo.startTask (Halo.Restartable Refresh) do +refreshTask = Halo.restartable Refresh \_ -> do left <- Halo.fork loadLeftPane right <- Halo.fork loadRightPane waitUntilReady @@ -55,12 +59,10 @@ Halo.startTask (Halo.Restartable Refresh) do Halo.kill right ``` -Returning immediately after `fork` cancels the child; it does not create a detached background process. +Returning immediately after `fork` cancels the child. Use `perform` for component-scoped work that must survive the current handler; use `fork` for subwork whose lifetime must not exceed its parent. ## Configure handlers -A spec has one cohesive `handlers` record: - ```purescript type Handlers props state action key = { onActivate :: HaloM props state action key Unit @@ -69,7 +71,7 @@ type Handlers props state action key = } ``` -Start with `defaultHandlers` and update only the fields you need: +Start with `defaultHandlers` and replace the fields you need: ```purescript handlers = Halo.defaultHandlers @@ -81,132 +83,103 @@ handlers = Halo.defaultHandlers ### `onActivate` -Halo calls `onActivate` for each React effect activation. React development StrictMode can perform setup, cleanup, then setup again for the same hook instance. Treat activation as repeatable, not exactly once. Avoid irreversible “run once” effects unless the external owner supplies idempotency. - -Activation execution is cancelled and commit-fenced on deactivation. If activation submits an explicit task, that task is also component-scoped and is cancelled on deactivation. +Halo calls this for each React effect activation. Development StrictMode can run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable. Activation work and tasks it performs are cancelled on deactivation. ### `onPropsChange previousProps` -Halo runs this handler when the props reference changes. The argument is the previous props. Read current props inside Halo with: - -```purescript -current <- Halo.props -``` - -The runtime keeps the latest handler record, error handler, and React update callbacks. A handler already running keeps the computation selected when it started. +Halo runs this when the props reference changes. The argument is the previous props; read current props with `Halo.props`. Halo always selects callbacks from the latest spec for new work. ### `onAction action` -Halo starts an action handler as soon as the action is dispatched into the active scope. A subscription emission also dispatches an action through this field. If the component is inactive, dispatch is ignored. - -Long waits in an action handler remain cancellable and do not block other handlers, but they are invisible to `Activity`. Prefer an explicit task when running/queued state or a concurrency policy matters. +Halo starts this when rendering code dispatches or a subscription emits an action. Dispatch while inactive is ignored. Long handler waits remain cancellable but are invisible to task activity; use a task when scheduling or renderable progress matters. ## Work with state and props -`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use normal state operations: +`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use ordinary `get`, `put`, and `modify_`. `Halo.props` reads the latest props. -```purescript -current <- get -modify_ _ { status = Loading } -put next -``` - -Halo mirrors committed state into React. A replaced or deactivated owner can still finish foreign work, but later `get`/`put`/`modify_` operations cannot commit stale Halo state. - -`Halo.props` reads the latest props rather than a render-time snapshot. Capture a value before starting a task when the task must use the value associated with the action: +When work becomes stale through replacement, cancellation, or deactivation, later Halo state operations cannot commit. Foreign effects that already occurred cannot be reversed. Capture render- or action-associated values as task input instead of relying on later props: ```purescript +submitTask = Halo.drop SubmitRequest submit + onAction Submit = do { form } <- Halo.props - Halo.startTask (Halo.Drop SubmitRequest) (submit form) + Halo.perform submitTask form ``` -## Select a task policy +## Choose a scheduling strategy + +Every task has a key, including concurrent tasks. -### `Every` +### `concurrent key implementation` -Starts every submitted task immediately. Tasks run concurrently and appear only in total activity because `Every` has no key. +Every performance starts immediately, including multiple performances for the same key. Use this for independent bounded work such as metrics. A high-rate producer can create unbounded concurrency. -Use it for independent bounded work such as metrics. A high-rate producer can create unbounded concurrency. +### `restartable key implementation` -### `Restartable key` +A performance synchronously fences all running work for the key, discards its queue, requests cancellation, and starts the new input. Use it for search and navigation where the newest request wins. -Synchronously fences all running tasks for `key`, discards queued tasks, requests cancellation, then starts the new task. Use it for latest-request-wins search and navigation. +### `drop key implementation` -Cancellation cannot undo an external effect that already happened. If server ordering matters, add idempotency or version checks at that boundary. +A performance starts only when the key has no running or queued work. Otherwise its input is discarded and `perform` returns normally. Use it for optional duplicate submissions. -### `Drop key` +### `enqueue key implementation` -Starts the task only when `key` has no running or queued work. Otherwise the submission is discarded and `startTask` still returns normally. Use it to prevent duplicate form submissions. +Every input is preserved FIFO and runs one at a time for the key. Different keys remain independent. The queue is unbounded, so bound the producer when input can exceed throughput. -### `Enqueue key` +### `keepLatest key implementation` -Runs every task for `key` first-in, first-out, one at a time. Different keys remain independent. Use it for ordered writes or per-resource uploads. +Current work may finish; only the newest queued input is retained. Intermediate queued inputs are discarded. Use it for autosave when in-flight writes should not be cancelled. -The queue is unbounded. Bound the producer or choose another policy when sustained input can exceed throughput. +## Understand task identity and shared keys -### `KeepLatest key` +The task value carries a key, but scheduling coordination is by key—not JavaScript object identity. Two definitions with the same key and strategy intentionally share one scheduler slot, cancellation boundary, and activity count. This supports separately named operations that must serialize together. -Lets the current task finish, keeps only the newest queued task, and discards intermediate queued submissions. Use it for autosave when cancelling an in-flight write is undesirable but stale pending writes have no value. +A key's first performed task establishes its strategy for the entire component runtime lifetime. Performing another definition with the same key and a different strategy is rejected: no work starts, and `onError` receives `TaskConfigurationError key` with an error naming both strategies. The association remains across StrictMode deactivate/reactivate cycles. This catches accidental key reuse while permitting deliberate same-strategy sharing. -Use one stable policy for a given key. Mixing policies is processed according to each arriving submission, but it makes the workflow harder to reason about. +Use distinct keys for independent work. Do not treat task input as identity: changing input creates another performance of the same task. -## Cancel keyed tasks explicitly +## Cancel a task -`cancelTask key` immediately fences running tasks for the key, discards its queue, requests fiber cancellation, updates activity, and returns. It does not affect unkeyed `Every` tasks. +`cancel task` synchronously fences every running performance and discards every queued input for the task's key, requests fiber cancellation, publishes activity, and returns. ```purescript onAction = case _ of - SearchChanged query -> - Halo.startTask (Halo.Restartable SearchRequest) (search query) + SearchChanged query -> Halo.perform searchTask query SearchCleared -> do - Halo.cancelTask SearchRequest + Halo.cancel searchTask modify_ _ { results = [] } ``` -If a keyed task cancels its own key, it fences and requests cancellation of itself as well as its keyed siblings. +Definitions sharing a key share cancellation. If a task cancels its own key, it fences itself and all keyed siblings. ## Render task activity -`component` renderers and `useHalo` return `Activity key`. Activity updates cause React renders. +`component` renderers and `useHalo` return `Activity key`: ```purescript let - search = Halo.activityFor SearchRequest halo.activity - total = Halo.activityTotals halo.activity + searchCounts = Halo.activity searchTask halo.activity + totalCounts = Halo.activityTotals halo.activity ``` -Each count has `{ running, queued }`. Totals include keyed tasks and unkeyed `Every` tasks. Per-key counts include keyed tasks only. +Each count is `{ running, queued }`. Every task is keyed, so totals are the sum of all keyed slots. Definitions sharing a key report the same counts. -Activity deliberately excludes: - -- activation, prop-change, and action handlers; -- structured `fork` children; and -- emitter subscription cleanup. - -This keeps the value precise: it represents only work submitted through `startTask`. +Activity includes only performed tasks. It excludes activation, prop-change, and action handlers, structured `fork` children, and subscription cleanup. ## Subscribe to custom emitters -Halo's small emitter type avoids a Halogen dependency: +Halo's emitter avoids a Halogen dependency: ```purescript -events :: Halo.Emitter Action events = Halo.makeEmitter \emit -> do listener <- source.listen emit pure (source.remove listener) ``` -Registration receives an action callback and returns an `Effect Unit` cleanup. Subscribe inside Halo: - -```purescript -subscriptionId <- Halo.subscribe events -Halo.unsubscribe subscriptionId -``` - -A subscription remains component-scoped after the creating handler finishes. Manual unsubscription removes cleanup from tracking before running it. Deactivation attempts every tracked cleanup, even when one throws, and reports each thrown cleanup as `DeactivationError` after cancellation requests have been issued. +`subscribe events` registers an action source in the current activation scope; `unsubscribe id` removes it early. Manual unsubscription removes tracking before cleanup runs. Deactivation attempts every tracked cleanup even when one throws, then reports failures as `DeactivationError`. -`Emitter` is broadcast-style. It is not a consuming queue and does not provide backpressure. Each emission dispatches one action to each registered Halo receiver. Choose task policies inside `onAction` when emitted actions start asynchronous work. +Emitters broadcast without consuming-queue or backpressure semantics. Each emission dispatches an action; the handler may perform a task with the appropriate pressure strategy. ## Handle unexpected errors @@ -221,25 +194,19 @@ Contexts are: - `ActivationError` for `onActivate`; - `PropsChangeError previousProps` for `onPropsChange`; - `ActionError action` for `onAction`; -- `TaskError policy` for an explicit task; and -- `DeactivationError` for a throwing subscription cleanup. +- `TaskError key` for performed task failure; +- `TaskConfigurationError key` for conflicting same-key strategies; and +- `DeactivationError` for throwing subscription cleanup. -Expected failures belong in domain state or actions. Catch them inside the task with `attempt`, `try`, or a domain-specific error type. Let genuinely unexpected failures reach `onError` for logging or reporting. Cancellation requested by Halo is suppressed rather than reported as an application error. +Catch expected domain failures inside the task and put them in state or dispatch a domain action. Let unexpected failures reach `onError`. Halo-initiated cancellation is suppressed. ## Understand cancellation limits -Replacement and deactivation do two things: - -1. mark the old owner inactive synchronously, blocking later Halo state commits and new Halo-owned capabilities; and -2. request cancellation of its `Aff` fibers. - -Cancellation is cooperative. It cannot retract an HTTP request, storage write, analytics event, or foreign callback already performed. Commit fencing protects Halo state, not external systems. Design external operations for retry, ordering, and idempotency when those properties matter. +Replacement, cancellation, and deactivation synchronously mark old owners stale, which blocks later Halo state commits and new Halo-owned capabilities, then request `Aff` cancellation. Cancellation is cooperative. It cannot retract an HTTP request, storage write, analytics event, or foreign callback already performed. Design external operations for retry, ordering, and idempotency where needed. ## Choose `component` or `useHalo` -Use `Halo.component` when Halo owns the full component. The renderer receives props, state, dispatch, and activity. - -Use `Halo.useHalo` when composing with other hooks: +Use `Halo.component` when Halo owns the whole component. Its renderer receives props, state, dispatch, and activity. Use `Halo.useHalo` when composing with other hooks: ```purescript halo <- Halo.useHalo @@ -250,55 +217,39 @@ halo <- Halo.useHalo } ``` -Read `halo.state`, call `halo.dispatch`, and render `halo.activity`. +Read `halo.state`, call `halo.dispatch`, and pass `halo.activity` to a task's `Halo.activity` helper. ## Common patterns -### Search with latest-request-wins - -Capture the query from the action and use `Restartable`: - -```purescript -SearchChanged query -> - Halo.startTask (Halo.Restartable SearchRequest) do - results <- liftAff $ fetchResults query - modify_ _ { query = query, results = results } -``` - -### Ignore duplicate saves - -```purescript -SaveClicked -> - Halo.startTask (Halo.Drop SaveRequest) saveCurrentForm -``` - -### Ordered work per resource - ```purescript -UploadChunk fileId chunk -> - Halo.startTask (Halo.Enqueue (Upload fileId)) (upload chunk) -``` - -### Cancel when input becomes empty +searchTask = Halo.restartable SearchRequest search +saveTask = Halo.drop SaveRequest saveCurrentForm +uploadTask fileId = Halo.enqueue (Upload fileId) uploadChunk +autosaveTask = Halo.keepLatest AutosaveRequest saveDraft +metricTask = Halo.concurrent Metrics recordMetric -```purescript -QueryChanged "" -> Halo.cancelTask SearchRequest -QueryChanged query -> - Halo.startTask (Halo.Restartable SearchRequest) (search query) +onAction = case _ of + SearchChanged query -> Halo.perform searchTask query + SaveClicked -> Halo.perform_ saveTask + UploadChunk fileId chunk -> Halo.perform (uploadTask fileId) chunk + DraftChanged draft -> Halo.perform autosaveTask draft + MetricRecorded metric -> Halo.perform metricTask metric ``` ## Troubleshooting and footguns -**My fork stops immediately.** Its parent returned. Use `startTask` for component-scoped work, or keep the parent alive while it owns the child. +**My fork stops immediately.** Its parent returned. Perform a component-scoped task, or keep the parent alive while it owns the child. + +**Activity is zero while work is running.** The work is probably in a handler or structured child. Only `perform`/`perform_` submissions count. -**Activity is zero while work is running.** The work is probably in a handler or structured child. Submit it with `startTask` if it is task activity. +**A performed task was rejected with `TaskConfigurationError`.** Two definitions reuse a key with different strategies. Give independent work distinct keys or make deliberately shared definitions use one strategy. -**A dropped task did not run an error or completion action.** `Drop` intentionally discards the computation when its key is busy. Put only optional work behind it, or reflect acceptance separately in state. +**A dropped input did not run cleanup or report an error.** `drop` never starts the implementation when busy. Put only optional work behind it. -**My queue keeps growing.** `Enqueue` has no built-in bound. Limit input, batch it, or use `KeepLatest`/`Drop`. +**My queue keeps growing.** `enqueue` is unbounded. Limit input, batch it, or use `keepLatest`/`drop`. -**A cancelled request still reached the server.** Halo can fence component commits and request `Aff` cancellation; it cannot undo an external side effect. +**A cancelled request still reached the server.** Halo fences component commits and requests cancellation; it cannot undo external effects. -**Initialization ran twice in development.** React StrictMode replayed effect activation. Make `onActivate` replay-safe. +**Initialization ran twice in development.** StrictMode replayed activation. Make `onActivate` replay-safe. -**An emitter overwhelms the component.** Emitters broadcast without backpressure. Reduce events at the source or let actions submit tasks with a pressure-appropriate policy. +**An emitter overwhelms the component.** Reduce events at the source or perform a task with an appropriate pressure strategy. diff --git a/docs/migration-v4.md b/docs/migration-v4.md index 3f8de6e..95e53f1 100644 --- a/docs/migration-v4.md +++ b/docs/migration-v4.md @@ -1,18 +1,18 @@ # Migrate from Halo v3 to v4 -Halo v4 is an unreleased breaking redesign. It replaces the Free/FreeAp evaluator and implicit action effects with a direct scoped runtime, named handlers, and explicit tasks. There are no compatibility aliases in v4. +Halo v4 is an unreleased breaking redesign. It replaces the Free/FreeAp evaluator and implicit action effects with a direct scoped runtime, named handlers, and first-class tasks. There are no compatibility aliases. ## Why the model changed -In v3, `eval` combined lifecycle events and actions, and action evaluation commonly became asynchronous work by convention. That made it difficult to tell whether an action was an event, a long-running task, or both. It also left concurrency policy and cancellation ownership implicit. +In v3, `eval` combined lifecycle events and actions, and asynchronous action work was conventional rather than explicit. It was difficult to tell whether an action was an event, a long-running task, or both, and concurrency ownership was easy to obscure. In v4: -- handlers respond to lifecycle and action events; -- `startTask` explicitly marks component-scoped asynchronous work; -- a `TaskPolicy` is chosen next to that work; +- handlers respond to activation, prop changes, and actions; +- reusable task definitions bind identity, scheduling strategy, and implementation; +- `perform` explicitly starts component-scoped work; - `fork` is explicitly parent-scoped; and -- activity counts explicit tasks only. +- activity counts performed tasks only. ## Migration sequence @@ -32,6 +32,8 @@ HaloM props state action key a Choose an application task-key type with an `Ord` instance. Halo now runs directly on `Aff`; remove the custom base monad parameter, `hoist`, `HaloAp`, and Free/FreeAp-specific code. Use `liftAff` for asynchronous effects. +Task input does not become another `HaloM` parameter. It is generic on each `Task` value. + ### 2. Replace `eval` with `handlers` Replace lifecycle pattern matching: @@ -56,41 +58,63 @@ handlers = Halo.defaultHandlers There is no public `Lifecycle`, `EvalSpec`, `mkEval`, or `defaultEval` in v4. -`onActivate` is repeatable under React StrictMode. There is no asynchronous deactivation handler: React cleanup is synchronous, and pretending otherwise would give misleading completion guarantees. Use subscription cleanup, `Aff` finalizers, or an external resource owner. +`onActivate` is repeatable under React StrictMode. There is no asynchronous deactivation handler: React cleanup is synchronous. Use subscription cleanup, `Aff` finalizers, or an external resource owner. -### 3. Make tasks explicit +### 3. Define long-running operations as tasks -In v3, an action handler might perform a request directly: +Create a key type, then define each task once: ```purescript -Action (SearchChanged query) -> do +data TaskKey = SearchRequest | SaveRequest + +derive instance eqTaskKey :: Eq TaskKey +derive instance ordTaskKey :: Ord TaskKey + +searchTask :: Halo.Task Props State Action TaskKey String +searchTask = Halo.restartable SearchRequest \query -> do results <- liftAff $ search query modify_ _ { results = results } + +saveTask :: Halo.Task Props State Action TaskKey Unit +saveTask = Halo.drop SaveRequest \_ -> saveCurrentForm ``` -In v4, submit work with its policy: +The available constructors are `concurrent`, `restartable`, `drop`, `enqueue`, and `keepLatest`. Each takes a key and an input-driven implementation. Strategy is part of the definition and cannot vary at performance sites. + +A key's first performance establishes its strategy for the component runtime lifetime. Deliberate same-key, same-strategy definitions share a slot. Conflicting same-key strategies are rejected through `TaskConfigurationError key`. + +### 4. Perform tasks from actions + +Replace direct long-running action work or any action-to-policy table with: ```purescript onAction = case _ of - SearchChanged query -> - Halo.startTask (Halo.Restartable SearchRequest) do - results <- liftAff $ search query - modify_ _ { results = results } + SearchChanged query -> Halo.perform searchTask query + SaveClicked -> Halo.perform_ saveTask ``` -Delete any top-level `schedule :: action -> TaskPolicy key`. An action is no longer implicitly a task. Some actions may only modify state; others may submit multiple tasks or cancel a keyed task. +An action is no longer implicitly a task. Some actions only update state; others may perform multiple tasks. -### 4. Add explicit keyed cancellation where needed +### 5. Replace keyed cancellation and activity lookup -Replace stored task fibers or cancellation actions with: +Cancellation now takes the task definition: ```purescript -Halo.cancelTask SearchRequest +Halo.cancel searchTask ``` -This cancels running keyed tasks and discards their queue. It does not affect `Every` tasks. +This fences running work and discards queued work for the task's key. Same-key definitions share cancellation. -### 5. Update the error handler +Activity lookup also takes the task: + +```purescript +searchCounts = Halo.activity searchTask halo.activity +totalCounts = Halo.activityTotals halo.activity +``` + +Every task, including `concurrent`, is keyed. Replace direct key lookup with the task-based helper. + +### 6. Update the error handler Change: @@ -104,17 +128,18 @@ into: onError :: ErrorContext props action key -> Error -> Effect Unit ``` -Handle the v4 contexts: +Handle: - `ActivationError`; - `DeactivationError` for subscription cleanup; - `PropsChangeError previousProps`; -- `ActionError action`; and -- `TaskError policy`. +- `ActionError action`; +- `TaskError key`; and +- `TaskConfigurationError key`. Expected request failures still belong in domain state or actions. -### 6. Update hook and component specs +### 7. Update hook and component specs Remove `eval` and `schedule`; add `handlers`: @@ -127,23 +152,23 @@ halo <- Halo.useHalo } ``` -`useHalo` returns a record with `state`, `dispatch`, and `activity`. - -`Halo.component` renderers receive `{ props, state, dispatch, activity }`. The old `send` field is now `dispatch`. +`useHalo` returns `state`, `dispatch`, and `activity`. `Halo.component` renderers receive `{ props, state, dispatch, activity }`; the old `send` field is now `dispatch`. -### 7. Revisit every `fork` +### 8. Revisit every `fork` -A v4 `fork` is a structured child. It is cancelled when its creating handler or task finishes. If the old code expected a fork to survive handler completion until component unmount, convert it to an explicit task: +A v4 `fork` is a structured child. It is cancelled when its creating handler or task finishes. If old code expected a fork to survive handler completion, make it a task and call `perform`: ```purescript -Halo.startTask (Halo.Restartable BackgroundSync) backgroundSync +backgroundSync = Halo.restartable BackgroundSync \_ -> synchronize + +onAction StartSync = Halo.perform_ backgroundSync ``` Use `fork` only for concurrency owned by a parent that remains alive. -### 8. Replace Halogen emitters +### 9. Replace Halogen emitters -Halo v4 has its own small emitter type: +Halo v4 has its own emitter type: ```purescript events = Halo.makeEmitter \emit -> do @@ -158,11 +183,12 @@ events = Halo.makeEmitter \emit -> do Before completing a migration, verify: - `onActivate` is safe to replay; -- each long-running operation uses an intentional policy; -- `Drop` submissions are genuinely optional; -- `Enqueue` producers cannot grow an unbounded queue unexpectedly; -- `cancelTask` is used when UI state must clear keyed work without replacement; -- activity-dependent UI expects explicit tasks only; -- structured children do not need to outlive their parents; +- task keys are stable and distinct where work is independent; +- definitions sharing a key use one intentional strategy; +- `drop` inputs are genuinely optional; +- `enqueue` producers cannot grow an unbounded queue unexpectedly; +- `cancel task` is used when UI state must clear work without replacement; +- activity-dependent UI expects performed tasks only; +- structured children do not need to outlive parents; - expected failures are modeled in state rather than logged as unexpected errors; and - external writes remain correct even when local cancellation cannot undo them. diff --git a/docs/reference.md b/docs/reference.md index fe0db03..6987ec4 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -6,7 +6,7 @@ Import the intentional public surface from `React.Halo`: import React.Halo as Halo ``` -Runtime constructors and ownership records are internal and not exported from this module. +Runtime constructors, task representation, and scheduling strategies are internal. ## Core computation @@ -14,15 +14,9 @@ Runtime constructors and ownership records are internal and not exported from th HaloM props state action key a ``` -`HaloM` runs directly on `Aff` in a private scoped environment. It has `Functor`, `Apply`, `Applicative`, `Bind`, `Monad`, `MonadState state`, `MonadEffect`, and `MonadAff` instances. +`HaloM` runs on `Aff` in a private scoped environment. It has `Functor`, `Apply`, `Applicative`, `Bind`, `Monad`, `MonadState state`, `MonadEffect`, and `MonadAff` instances. -Type parameters: - -- `props`: current React component props; -- `state`: Halo-owned component state; -- `action`: events accepted by `dispatch` and subscriptions; -- `key`: application-defined explicit task keys; and -- `a`: computation result. +The parameters are component props, Halo state, dispatched actions, application task keys, and the result. Task input is generic on each `Task`; it is deliberately not another `HaloM` parameter. ## Handlers @@ -36,47 +30,78 @@ type Handlers props state action key = defaultHandlers :: forall props state action key. Handlers props state action key ``` -`defaultHandlers` ignores all callbacks. Use PureScript record update syntax to replace selected fields. +`defaultHandlers` ignores every callback. Handlers are active-scope-owned, concurrent, commit-fenced, and excluded from task activity. + +- `onActivate` runs for every React effect activation. +- `onAction` starts for each action dispatched while active. +- `onPropsChange previousProps` starts when the props reference changes; use `props` for current props. + +## Task definitions + +```purescript +Task props state action key input +``` + +`Task` is abstract. It binds a key, a strategy, and an `input -> HaloM ... Unit` implementation. + +```purescript +concurrent + :: key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input + +restartable + :: key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input + +drop + :: key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input + +enqueue + :: key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input + +keepLatest + :: key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input +``` -- `onActivate` runs for every React effect activation and may run more than once for one hook instance. -- `onAction` starts for each action dispatched while the scope is active. -- `onPropsChange previousProps` starts when the props reference changes. Read current props with `props`. +- `concurrent`: every performance starts immediately, including same-key work. +- `restartable`: fences/cancels running work, discards queued work, then starts the new input. +- `drop`: discards the new input while the key is busy. +- `enqueue`: preserves every input FIFO and runs one at a time. +- `keepLatest`: lets running work finish and retains only the newest queued input. -Handlers are scope-owned, concurrent, and excluded from `Activity`. +The first performed definition for a key fixes that key's strategy for the component runtime lifetime. Same key plus same strategy shares a slot. Same key plus a different strategy is rejected through `onError` as `TaskConfigurationError key`, including across deactivate/reactivate. -## Task submission and cancellation +## Performance and cancellation ```purescript -startTask +perform :: Ord key - => TaskPolicy key + => Task props state action key input + -> input -> HaloM props state action key Unit + +perform_ + :: Ord key + => Task props state action key Unit -> HaloM props state action key Unit -cancelTask +cancel :: Ord key - => key + => Task props state action key input -> HaloM props state action key Unit ``` -`startTask` submits component-scoped work and returns without waiting for it. The task outlives successful completion of the submitting handler or task. It is cancelled on scope deactivation. +`perform` and `perform_` submit component-scoped work and return without waiting. The submitted work outlives successful completion of its caller and is cancelled on scope deactivation. -`cancelTask key` synchronously fences running tasks for `key`, discards the queue, requests cancellation, publishes new activity, and returns. It cannot target unkeyed `Every` tasks. - -```purescript -data TaskPolicy key - = Every - | Restartable key - | Drop key - | Enqueue key - | KeepLatest key -``` - -- `Every`: starts all submissions concurrently. -- `Restartable key`: replaces running and queued work for the key. -- `Drop key`: discards a submission while the key is busy. -- `Enqueue key`: runs every submission FIFO, one at a time. -- `KeepLatest key`: lets current work finish and retains only the newest queued submission. +`cancel` synchronously fences running work and discards queued work for the task's key, requests cancellation, updates activity, and returns. All definitions sharing the key share this boundary. ## Activity @@ -86,12 +111,17 @@ type TaskCounts = , queued :: Int } +activity + :: Ord key + => Task props state action key input + -> Activity key + -> TaskCounts + activityTotals :: Activity key -> TaskCounts -activityFor :: Ord key => key -> Activity key -> TaskCounts emptyActivity :: Activity key ``` -`Activity` counts explicit `startTask` submissions only. Totals include unkeyed and keyed tasks. `activityFor` reports one keyed slot. Handler execution and structured children are excluded. +`activity task snapshot` reports the task key's slot; same-key definitions report the same counts. `activityTotals` sums every slot. Activity counts only `perform`/`perform_` submissions, not handlers, structured children, or subscriptions. ## State and props @@ -113,7 +143,7 @@ kill -> HaloM props state action key Unit ``` -`fork` creates a concurrent child owned by the current handler or task. Parent completion or cancellation cancels the child. `kill` requests earlier cancellation. `ForkId` is abstract from `React.Halo`. +`fork` creates a concurrent child owned by the current handler or performed task. Parent completion or cancellation cancels the child. `kill` requests earlier cancellation. `ForkId` is abstract. ## Subscriptions and emitters @@ -137,11 +167,7 @@ unsubscribe -> HaloM props state action key Unit ``` -Emitter registration receives a receiver and returns its cleanup effect. Subscription emissions dispatch actions into the activation scope that registered them. Stale callbacks cannot target a later scope. - -Manual unsubscription removes tracking before cleanup runs. Deactivation attempts all remaining cleanup effects; throwing cleanup is reported as `DeactivationError` without preventing other cleanup and cancellation requests. - -`SubscriptionId` is abstract from `React.Halo`. +Emitter registration receives a receiver and returns cleanup. Emissions dispatch actions into the registering activation scope; stale callbacks cannot target a later scope. Manual unsubscribe removes tracking before cleanup. Deactivation attempts all remaining cleanup, and reports thrown cleanup as `DeactivationError` without preventing other cleanup and cancellation. ## Errors @@ -151,16 +177,13 @@ data ErrorContext props action key | DeactivationError | PropsChangeError props | ActionError action - | TaskError (TaskPolicy key) -``` + | TaskError key + | TaskConfigurationError key -Every hook or component spec supplies: - -```purescript onError :: ErrorContext props action key -> Error -> Effect Unit ``` -Halo sends unexpected handler and task failures to this callback. `DeactivationError` is reserved for throwing subscription cleanup. Halo suppresses cancellation errors it initiated. +Unexpected handler and task failures reach `onError`. `TaskError` identifies the task key. `TaskConfigurationError` identifies a same-key strategy conflict. `DeactivationError` reports throwing subscription cleanup. Halo suppresses cancellation errors it initiated. ## Hook API @@ -184,7 +207,7 @@ useHalo -> Hook (UseHalo props state action key) (HaloHook state action key) ``` -The hook synchronizes the latest handlers and callbacks on each React effect cycle. Activation cleanup deactivates the owned scope; StrictMode reactivation creates a fresh scope. +The hook synchronizes the latest handlers and React callbacks. Cleanup deactivates its scope; StrictMode reactivation creates a fresh scope while retaining the runtime's key-strategy validation. ## Component API @@ -209,4 +232,4 @@ component -> Component props ``` -Use `component` when Halo owns the entire component. Use `useHalo` when other React hooks share the render function. +Use `component` when Halo owns the whole component. Use `useHalo` when other React hooks share the render function. diff --git a/src/React/Halo.purs b/src/React/Halo.purs index 9a37038..3ffb070 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -5,6 +5,7 @@ module React.Halo import React.Halo.Component (ComponentSpec, component) as Exports import React.Halo.Handlers (Handlers, defaultHandlers) as Exports import React.Halo.Hook (HaloHook, HookSpec, UseHalo(..), useHalo) as Exports -import React.Halo.Internal.Runtime (HaloM, cancelTask, fork, kill, props, startTask, subscribe, subscribe', unsubscribe) as Exports -import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, SubscriptionId, TaskCounts, TaskPolicy(..), activityFor, activityTotals, emptyActivity) as Exports +import React.Halo.Internal.Runtime (HaloM, fork, kill, props, subscribe, subscribe', unsubscribe) as Exports +import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, SubscriptionId, TaskCounts, activityTotals, emptyActivity) as Exports import React.Halo.Subscription (Emitter, makeEmitter) as Exports +import React.Halo.Task (Task, activity, cancel, concurrent, drop, enqueue, keepLatest, perform, perform_, restartable) as Exports diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index 9b28bef..21a11f1 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -3,14 +3,14 @@ module React.Halo.Internal.Runtime , Handlers , Runtime , activate - , cancelTask + , cancelDefinition , createRuntime , deactivate , dispatch , fork , kill + , performTask , props - , startTask , subscribe , subscribe' , syncSpec @@ -40,7 +40,9 @@ import Effect.Class (class MonadEffect, liftEffect) import Effect.Exception as Exception import Effect.Ref (Ref) import Effect.Ref as Ref -import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), SubscriptionId(..), TaskPolicy(..), emptyActivity) +import React.Halo.Internal.Task (Strategy(..), Task) +import React.Halo.Internal.Task as Task +import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), SubscriptionId(..), emptyActivity) import React.Halo.Subscription (Emitter) import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) @@ -58,8 +60,8 @@ derive newtype instance monadHaloM :: Monad (HaloM props state action key) derive newtype instance monadEffectHaloM :: MonadEffect (HaloM props state action key) derive newtype instance monadAffHaloM :: MonadAff (HaloM props state action key) --- | Activation, prop-change, and action callbacks. Handlers run immediately as scope-owned --- | computations; only work submitted with `startTask` enters the task scheduler. +-- | Activation, prop-change, and action callbacks. Handlers run immediately as +-- | scope-owned computations; only work submitted with `perform` enters the task scheduler. type Handlers props state action key = { onActivate :: HaloM props state action key Unit , onAction :: action -> HaloM props state action key Unit @@ -77,6 +79,7 @@ newtype Runtime props state action key = Runtime , props :: Ref props , scope :: Ref (Maybe (Scope props state action key)) , spec :: Ref (RuntimeSpec props state action key) + , strategies :: Ref (Map key Strategy) , state :: Ref state , stateUpdate :: Ref (state -> Effect Unit) } @@ -84,7 +87,6 @@ newtype Runtime props state action key = Runtime newtype Scope :: Type -> Type -> Type -> Type -> Type newtype Scope props state action key = Scope { active :: Ref Boolean - , every :: Ref (Map Int (Root props state action key)) , generation :: Int , roots :: Ref (Map Int (Root props state action key)) , subscriptions :: Ref (Map SubscriptionId (Effect Unit)) @@ -93,9 +95,11 @@ newtype Scope props state action key = Scope type TaskRequest :: Type -> Type -> Type -> Type -> Type type TaskRequest props state action key = - { computation :: HaloM props state action key Unit - , policy :: TaskPolicy key - } + { computation :: HaloM props state action key Unit } + +data StrategyRegistration + = StrategyAccepted + | StrategyConflict Strategy type TaskSlot :: Type -> Type -> Type -> Type -> Type type TaskSlot props state action key = @@ -164,6 +168,7 @@ createRuntime input = do scope <- Ref.new Nothing spec <- Ref.new input.spec state <- Ref.new input.initialState + strategies <- Ref.new Map.empty stateUpdate <- Ref.new input.stateUpdate pure $ Runtime { activityUpdate @@ -172,6 +177,7 @@ createRuntime input = do , scope , spec , state + , strategies , stateUpdate } @@ -196,11 +202,10 @@ activate runtime@(Runtime state) = do Nothing -> do generation <- fresh runtime active <- Ref.new true - every <- Ref.new Map.empty roots <- Ref.new Map.empty subscriptions <- Ref.new Map.empty tasks <- Ref.new Map.empty - let scope = Scope { active, every, generation, roots, subscriptions, tasks } + let scope = Scope { active, generation, roots, subscriptions, tasks } Ref.write (Just scope) state.scope spec <- Ref.read state.spec startHandler runtime scope ActivationError spec.handlers.onActivate @@ -215,14 +220,12 @@ deactivate runtime@(Runtime state) = do Ref.write Nothing state.scope roots <- takeRef current.roots Map.empty - every <- takeRef current.every Map.empty tasks <- takeRef current.tasks Map.empty subscriptions <- takeRef current.subscriptions Map.empty publishActivity runtime emptyActivity cleanupResults <- traverse Exception.try (Map.values subscriptions) traverse_ cancelRoot (Map.values roots) - traverse_ cancelRoot (Map.values every) traverse_ (traverse_ cancelRoot <<< Map.values <<< _.running) (Map.values tasks) -- A faulty external cleanup must not prevent the rest of the scope from @@ -284,34 +287,45 @@ props = HaloM do let Runtime runtime = execution.runtime liftEffect $ Ref.read runtime.props --- | Submit a component-scoped task and return immediately. The task is owned by --- | the active scope rather than by the handler or task that submitted it. -startTask - :: forall props state action key +-- | Internal capability used by the abstract public Task API. +performTask + :: forall props state action key input . Ord key - => TaskPolicy key - -> HaloM props state action key Unit + => Task (HaloM props state action key) key input + -> input -> HaloM props state action key Unit -startTask policy computation = HaloM do +performTask task input = HaloM do execution <- ask liftEffect do current <- isCurrent execution - when current $ - scheduleTask execution.runtime execution.scope { computation, policy } - --- | Fence and cancel running tasks for a key and discard every queued task for --- | that key. Unkeyed `Every` tasks are unaffected. -cancelTask - :: forall props state action key + when current do + configured <- registerStrategy execution.runtime (Task.key task) (Task.strategy task) + case configured of + StrategyAccepted -> + scheduleTask execution.runtime execution.scope (Task.key task) (Task.strategy task) + { computation: Task.run task input } + StrategyConflict previous -> do + let Runtime runtime = execution.runtime + spec <- Ref.read runtime.spec + spec.onError (TaskConfigurationError (Task.key task)) + ( Exception.error $ + "Task key was already defined as " <> Task.strategyName previous + <> " and cannot also be defined as " + <> Task.strategyName (Task.strategy task) + ) + +-- | Internal capability used by the abstract public Task API. +cancelDefinition + :: forall props state action key input . Ord key - => key + => Task (HaloM props state action key) key input -> HaloM props state action key Unit -cancelTask key = HaloM do +cancelDefinition task = HaloM do execution <- ask liftEffect do current <- isCurrent execution when current $ - cancelKeyedTasks execution.runtime execution.scope key + cancelKeyedTasks execution.runtime execution.scope (Task.key task) -- | Register an emitter in the active component scope. Its cleanup runs on -- | manual unsubscription or scope deactivation. @@ -411,31 +425,45 @@ startHandler runtime scope@(Scope current) context computation = do Ref.modify_ (Map.insert runId prepared.root) current.roots prepared.start +registerStrategy + :: forall props state action key + . Ord key + => Runtime props state action key + -> key + -> Strategy + -> Effect StrategyRegistration +registerStrategy (Runtime runtime) key requested = Ref.modify' update runtime.strategies + where + update strategies = case Map.lookup key strategies of + Nothing -> + { state: Map.insert key requested strategies + , value: StrategyAccepted + } + Just existing | existing == requested -> + { state: strategies, value: StrategyAccepted } + Just existing -> + { state: strategies, value: StrategyConflict existing } + scheduleTask :: forall props state action key . Ord key => Runtime props state action key -> Scope props state action key + -> key + -> Strategy -> TaskRequest props state action key -> Effect Unit -scheduleTask runtime scope@(Scope current) request = case request.policy of - Every -> do - runId <- fresh runtime - prepared <- prepare Nothing runtime scope (TaskError request.policy) request.computation \_ -> do - Ref.modify_ (Map.delete runId) current.every - notifyActivity runtime scope - Ref.modify_ (Map.insert runId prepared.root) current.every - notifyActivity runtime scope - prepared.start - Restartable key -> do +scheduleTask runtime scope@(Scope current) key strategy request = case strategy of + Concurrent -> startKeyed runtime scope key request + Restartable -> do cancelKeyedTasks runtime scope key startKeyed runtime scope key request - Drop key -> do + Drop -> do tasks <- Ref.read current.tasks let busy = maybe false (\slot -> not Map.isEmpty slot.running || not Array.null slot.queued) (Map.lookup key tasks) unless busy $ startKeyed runtime scope key request - Enqueue key -> enqueueOrStart runtime scope key request false - KeepLatest key -> enqueueOrStart runtime scope key request true + Enqueue -> enqueueOrStart runtime scope key request false + KeepLatest -> enqueueOrStart runtime scope key request true cancelKeyedTasks :: forall props state action key @@ -461,7 +489,7 @@ startKeyed -> Effect Unit startKeyed runtime scope@(Scope current) key request = do runId <- fresh runtime - prepared <- prepare Nothing runtime scope (TaskError request.policy) request.computation \_ -> + prepared <- prepare Nothing runtime scope (TaskError key) request.computation \_ -> completeKeyed runtime scope key runId Ref.modify_ (Map.alter (Just <<< addRun runId prepared.root <<< maybe emptySlot identity) key) current.tasks notifyActivity runtime scope @@ -609,7 +637,6 @@ notifyActivity notifyActivity runtime@(Runtime state) scope@(Scope current) = do active <- isScopeCurrent runtime scope when active do - every <- Ref.read current.every tasks <- Ref.read current.tasks let counts slot = @@ -617,11 +644,8 @@ notifyActivity runtime@(Runtime state) scope@(Scope current) = do , queued: Array.length slot.queued } byKey = map counts tasks - keyed = foldl addCounts { running: 0, queued: 0 } (Map.values byKey) - activity = Activity - { total: keyed { running = keyed.running + Map.size every } - , byKey - } + total = foldl addCounts { running: 0, queued: 0 } (Map.values byKey) + activity = Activity { total, byKey } update <- Ref.read state.activityUpdate update activity diff --git a/src/React/Halo/Internal/Task.purs b/src/React/Halo/Internal/Task.purs new file mode 100644 index 0000000..02058c1 --- /dev/null +++ b/src/React/Halo/Internal/Task.purs @@ -0,0 +1,41 @@ +module React.Halo.Internal.Task + ( Strategy(..) + , Task(..) + , key + , run + , strategy + , strategyName + ) where + +import Prelude + +-- | Runtime-only scheduling modes. Public code chooses one by constructing a +-- | first-class task definition. +data Strategy + = Concurrent + | Restartable + | Drop + | Enqueue + | KeepLatest + +derive instance eqStrategy :: Eq Strategy + +-- | Internal representation parameterized by its computation monad. +data Task m key input = Task key Strategy (input -> m Unit) + +key :: forall m key input. Task m key input -> key +key (Task taskKey _ _) = taskKey + +strategy :: forall m key input. Task m key input -> Strategy +strategy (Task _ taskStrategy _) = taskStrategy + +run :: forall m key input. Task m key input -> input -> m Unit +run (Task _ _ implementation) = implementation + +strategyName :: Strategy -> String +strategyName = case _ of + Concurrent -> "concurrent" + Restartable -> "restartable" + Drop -> "drop" + Enqueue -> "enqueue" + KeepLatest -> "keepLatest" diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index e310610..b96111d 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -4,8 +4,7 @@ module React.Halo.Internal.Types , ForkId(..) , SubscriptionId(..) , TaskCounts - , TaskPolicy(..) - , activityFor + , activityAtKey , activityTotals , emptyActivity ) where @@ -16,30 +15,17 @@ import Data.Map (Map) import Data.Map as Map import Data.Maybe (Maybe(..)) --- | Scheduling semantics for an explicit component-scoped task. --- | --- | `Every` starts every submitted task concurrently and is unkeyed. --- | `Restartable key` cancels prior running work and discards its queue. --- | `Drop key` ignores a submission while that key is busy. `Enqueue key` runs --- | every submission FIFO, one at a time. `KeepLatest key` lets the current task --- | finish while retaining only the newest queued submission. -data TaskPolicy key - = Every - | Restartable key - | Drop key - | Enqueue key - | KeepLatest key - -- | Identifies the operation whose unexpected failure reached `onError`. -- | --- | `PropsChangeError` carries the previous props. `TaskError` carries the --- | policy used when the explicit task was submitted. +-- | `PropsChangeError` carries the previous props. Task failures and task +-- | configuration conflicts carry the affected task key. data ErrorContext props action key = ActivationError | DeactivationError | PropsChangeError props | ActionError action - | TaskError (TaskPolicy key) + | TaskError key + | TaskConfigurationError key -- | Counts of explicit scheduled tasks. Handler and structured-child execution -- | is intentionally excluded. @@ -48,8 +34,8 @@ type TaskCounts = , queued :: Int } --- | A renderable snapshot of explicit task activity. Unkeyed `Every` tasks --- | appear in totals but not under a key. +-- | A renderable snapshot of explicit task activity. Every task is keyed, so +-- | the total is the sum of the per-key counts. newtype Activity key = Activity { total :: TaskCounts , byKey :: Map key TaskCounts @@ -59,6 +45,7 @@ derive newtype instance eqActivity :: Eq key => Eq (Activity key) derive newtype instance showActivity :: Show key => Show (Activity key) +-- | An activity snapshot with no running or queued tasks. emptyActivity :: forall key. Activity key emptyActivity = Activity { total: { running: 0, queued: 0 } @@ -69,9 +56,9 @@ emptyActivity = Activity activityTotals :: forall key. Activity key -> TaskCounts activityTotals (Activity activity) = activity.total --- | Read explicit task counts for one key. -activityFor :: forall key. Ord key => key -> Activity key -> TaskCounts -activityFor key (Activity activity) = +-- | Internal keyed lookup used by the abstract Task API. +activityAtKey :: forall key. Ord key => key -> Activity key -> TaskCounts +activityAtKey key (Activity activity) = case Map.lookup key activity.byKey of Just counts -> counts Nothing -> { running: 0, queued: 0 } diff --git a/src/React/Halo/Task.purs b/src/React/Halo/Task.purs new file mode 100644 index 0000000..1f8bc76 --- /dev/null +++ b/src/React/Halo/Task.purs @@ -0,0 +1,106 @@ +module React.Halo.Task + ( Task + , activity + , cancel + , concurrent + , drop + , enqueue + , keepLatest + , perform + , perform_ + , restartable + ) where + +import Prelude + +import React.Halo.Internal.Runtime (HaloM) +import React.Halo.Internal.Runtime as Runtime +import React.Halo.Internal.Task as Internal +import React.Halo.Internal.Types (Activity, TaskCounts, activityAtKey) + +-- | A reusable task definition. A task binds a user-defined key, one scheduling +-- | strategy, and an input-driven Halo computation. Its constructor is hidden; +-- | create tasks with `concurrent`, `restartable`, `drop`, `enqueue`, or +-- | `keepLatest`. +newtype Task props state action key input = Task + (Internal.Task (HaloM props state action key) key input) + +-- | Define a task whose performances for this key all run concurrently. +concurrent + :: forall props state action key input + . key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input +concurrent key implementation = Task (Internal.Task key Internal.Concurrent implementation) + +-- | Define a task whose newest performance cancels and replaces running and +-- | queued work for this key. +restartable + :: forall props state action key input + . key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input +restartable key implementation = Task (Internal.Task key Internal.Restartable implementation) + +-- | Define a task that ignores a performance while this key is busy. +drop + :: forall props state action key input + . key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input +drop key implementation = Task (Internal.Task key Internal.Drop implementation) + +-- | Define a task that runs every performance for this key FIFO, one at a time. +enqueue + :: forall props state action key input + . key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input +enqueue key implementation = Task (Internal.Task key Internal.Enqueue implementation) + +-- | Define a task that lets current work finish while retaining only the newest +-- | queued performance for this key. +keepLatest + :: forall props state action key input + . key + -> (input -> HaloM props state action key Unit) + -> Task props state action key input +keepLatest key implementation = Task (Internal.Task key Internal.KeepLatest implementation) + +-- | Submit one task input and return immediately. The resulting work belongs to +-- | the active component scope, not to the handler or task that submitted it. +perform + :: forall props state action key input + . Ord key + => Task props state action key input + -> input + -> HaloM props state action key Unit +perform (Task task) = Runtime.performTask task + +-- | Submit a task whose input is `Unit`. +perform_ + :: forall props state action key + . Ord key + => Task props state action key Unit + -> HaloM props state action key Unit +perform_ task = perform task unit + +-- | Fence and cancel all running work and discard all queued work for the task's +-- | key. Definitions that intentionally share the key share this cancellation +-- | boundary. +cancel + :: forall props state action key input + . Ord key + => Task props state action key input + -> HaloM props state action key Unit +cancel (Task task) = Runtime.cancelDefinition task + +-- | Read running and queued activity for the task's key. Definitions that share +-- | the key report the same counts. +activity + :: forall props state action key input + . Ord key + => Task props state action key input + -> Activity key + -> TaskCounts +activity (Task task) = activityAtKey (Internal.key task) diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index c96e4ae..b97effb 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -23,31 +23,33 @@ type State = data Action = Load -data Task = GreetingRequest - -derive instance eqTask :: Eq Task -derive instance ordTask :: Ord Task +data TaskKey = GreetingRequest + +derive instance eqTaskKey :: Eq TaskKey +derive instance ordTaskKey :: Ord TaskKey + +loadGreetingTask :: Halo.Task Props State Action TaskKey Unit +loadGreetingTask = Halo.restartable GreetingRequest \_ -> do + modify_ _ { loading = true, result = Nothing } + Props { loadGreeting } <- Halo.props + outcome <- liftAff $ attempt loadGreeting + modify_ _ + { loading = false + , result = Just $ case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + } loadButton :: Component Props loadButton = Halo.component "LoadButton" { initialState: \_ -> { loading: false, result: Nothing } , handlers: Halo.defaultHandlers - { onAction = \Load -> Halo.startTask (Halo.Restartable GreetingRequest) do - modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.props - outcome <- liftAff $ attempt loadGreeting - modify_ _ - { loading = false - , result = Just $ case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting - } - } + { onAction = \Load -> Halo.perform_ loadGreetingTask } , onError: \context error -> Console.error $ "Unexpected Halo failure in " <> showContext context <> ": " <> message error , render: \{ state, dispatch, activity } -> let - counts = Halo.activityFor GreetingRequest activity + counts = Halo.activity loadGreetingTask activity in R.div_ [ R.button @@ -61,13 +63,14 @@ loadButton = Halo.component "LoadButton" ] } -showContext :: Halo.ErrorContext Props Action Task -> String +showContext :: Halo.ErrorContext Props Action TaskKey -> String showContext = case _ of Halo.ActivationError -> "activation" Halo.DeactivationError -> "deactivation" Halo.PropsChangeError _ -> "props change" Halo.ActionError Load -> "Load action" - Halo.TaskError _ -> "greeting task" + Halo.TaskError GreetingRequest -> "greeting task" + Halo.TaskConfigurationError GreetingRequest -> "greeting task definition" data WorkflowAction = SearchChanged String @@ -81,19 +84,35 @@ data WorkflowTask | SaveRequest | AutosaveRequest | Upload Int + | Metrics derive instance eqWorkflowTask :: Eq WorkflowTask derive instance ordWorkflowTask :: Ord WorkflowTask +searchTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask String +searchTask = Halo.restartable SearchRequest \_ -> pure unit + +saveTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask Unit +saveTask = Halo.drop SaveRequest \_ -> pure unit + +autosaveTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask String +autosaveTask = Halo.keepLatest AutosaveRequest \_ -> pure unit + +uploadTask :: Int -> Halo.Task Unit Unit WorkflowAction WorkflowTask Int +uploadTask fileId = Halo.enqueue (Upload fileId) \_ -> pure unit + +metricTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask String +metricTask = Halo.concurrent Metrics \_ -> pure unit + handleWorkflow :: WorkflowAction -> Halo.HaloM Unit Unit WorkflowAction WorkflowTask Unit handleWorkflow = case _ of - SearchChanged _ -> Halo.startTask (Halo.Restartable SearchRequest) (pure unit) - SaveClicked -> Halo.startTask (Halo.Drop SaveRequest) (pure unit) - Autosave _ -> Halo.startTask (Halo.KeepLatest AutosaveRequest) (pure unit) - UploadChunk fileId _ -> Halo.startTask (Halo.Enqueue (Upload fileId)) (pure unit) - RecordMetric _ -> Halo.startTask Halo.Every (pure unit) + SearchChanged query -> Halo.perform searchTask query + SaveClicked -> Halo.perform_ saveTask + Autosave draft -> Halo.perform autosaveTask draft + UploadChunk fileId chunk -> Halo.perform (uploadTask fileId) chunk + RecordMetric name -> Halo.perform metricTask name data SimpleAction = InitializeData diff --git a/test/Test/Halo/Helpers.purs b/test/Test/Halo/Helpers.purs index cd3f9b5..e424349 100644 --- a/test/Test/Halo/Helpers.purs +++ b/test/Test/Halo/Helpers.purs @@ -3,6 +3,9 @@ module Test.Halo.Helpers , Gate , Harness , Key(..) + , UnitTask + , WorkInput + , WorkTask , await , awaitCounts , handlers @@ -12,6 +15,7 @@ module Test.Halo.Helpers , runGate , shouldNotHaveStarted , withHarness + , work ) where import Prelude @@ -32,9 +36,10 @@ import Effect.Class (liftEffect) import Effect.Exception (message) import Effect.Ref (Ref) import Effect.Ref as Ref +import React.Halo as Halo import React.Halo.Handlers (Handlers, defaultHandlers) -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, cancelTask, createRuntime, deactivate, fork, startTask) -import React.Halo.Internal.Types (Activity, ErrorContext(..), TaskCounts, TaskPolicy, activityTotals, emptyActivity) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, fork) +import React.Halo.Internal.Types (Activity, ErrorContext(..), TaskCounts, activityTotals, emptyActivity) import Test.Spec.Assertions (fail, shouldEqual) data Key = Search | Save @@ -53,13 +58,22 @@ type Gate = , started :: AVar Unit } +type WorkInput = + { gate :: Gate + , value :: Int + } + +type WorkTask = Halo.Task Unit (Array Int) Action Key WorkInput + +type UnitTask = Halo.Task Unit (Array Int) Action Key Unit + data Action - = StartTask (TaskPolicy Key) Int Gate - | StartTaskWithWitness (TaskPolicy Key) Int Gate Gate - | CancelTask Key (AVar Unit) + = Perform WorkTask WorkInput + | PerformWithWitness WorkTask WorkInput Gate + | PerformUnit UnitTask Gate + | Cancel WorkTask (AVar Unit) | Direct Int Gate | Boom Gate - | TaskBoom (TaskPolicy Key) Gate type Harness = { activity :: Ref (Activity Key) @@ -91,19 +105,25 @@ runGate value gate = do void $ AVar.take gate.release modify_ (flip Array.snoc value) +work :: WorkInput -> HaloM Unit (Array Int) Action Key Unit +work input = runGate input.value input.gate + handlers :: Handlers Unit (Array Int) Action Key handlers = defaultHandlers { onAction = case _ of - StartTask policy value gate -> do - startTask policy (runGate value gate) - liftAff $ void $ AVar.tryPut unit gate.launched - StartTaskWithWitness policy value gate witness -> do - startTask policy (runGate value gate) + Perform task input -> do + Halo.perform task input + liftAff $ void $ AVar.tryPut unit input.gate.launched + PerformWithWitness task input witness -> do + Halo.perform task input void $ fork (runGate 999 witness) liftAff $ void $ AVar.take witness.started + liftAff $ void $ AVar.tryPut unit input.gate.launched + PerformUnit task gate -> do + Halo.perform_ task liftAff $ void $ AVar.tryPut unit gate.launched - CancelTask key completed -> do - cancelTask key + Cancel task completed -> do + Halo.cancel task liftAff $ void $ AVar.tryPut unit completed Direct value gate -> do liftAff $ void $ AVar.tryPut unit gate.launched @@ -113,14 +133,6 @@ handlers = defaultHandlers liftAff $ Aff.finally (void $ AVar.tryPut unit gate.settled) (Aff.throwError (Aff.error "boom")) - TaskBoom policy gate -> do - startTask policy do - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - Aff.throwError (Aff.error "task boom") - liftAff $ void $ AVar.tryPut unit gate.launched } makeHarness :: Aff Harness @@ -184,4 +196,5 @@ contextName = case _ of DeactivationError -> "deactivation" PropsChangeError _ -> "props" ActionError _ -> "action" - TaskError _ -> "task" + TaskError key -> "task " <> show key + TaskConfigurationError key -> "task configuration " <> show key diff --git a/test/Test/Halo/SchedulerSpec.purs b/test/Test/Halo/SchedulerSpec.purs index daf25e3..6485a04 100644 --- a/test/Test/Halo/SchedulerSpec.purs +++ b/test/Test/Halo/SchedulerSpec.purs @@ -6,14 +6,14 @@ import Data.Array as Array import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Ref as Ref -import React.Halo.Internal.Runtime (dispatch) -import React.Halo.Internal.Types (TaskPolicy(..), activityFor) -import Test.Halo.Helpers (Action(..), Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness) +import React.Halo as Halo +import React.Halo.Internal.Runtime (activate, deactivate, dispatch) +import Test.Halo.Helpers (Action(..), Key(..), await, awaitCounts, makeGate, release, runGate, shouldNotHaveStarted, withHarness, work) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) spec :: Spec Unit -spec = describe "explicit task scheduling" do +spec = describe "first-class task scheduling" do it "handles an action immediately without counting it as task activity" $ withHarness \harness -> do gate <- liftEffect makeGate liftEffect $ dispatch harness.runtime (Direct 1 gate) @@ -25,175 +25,205 @@ spec = describe "explicit task scheduling" do state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1 ] - it "keeps a submitted task alive after its launching action completes" $ withHarness \harness -> do + it "keeps performed work alive after its launching handler completes" $ withHarness \harness -> do gate <- liftEffect makeGate witness <- liftEffect makeGate - liftEffect $ dispatch harness.runtime (StartTaskWithWitness Every 1 gate witness) + let task = Halo.concurrent Search work + liftEffect $ dispatch harness.runtime (PerformWithWitness task { value: 1, gate } witness) void $ await "task submission" gate.launched void $ await "launching handler completion" witness.settled - void $ await "submitted task start" gate.started + void $ await "performed task start" gate.started awaitCounts harness { running: 1, queued: 0 } release gate - void $ await "submitted task completion" gate.settled + void $ await "performed task completion" gate.settled awaitCounts harness { running: 0, queued: 0 } - it "Every runs every submitted task concurrently" $ withHarness \harness -> do + it "concurrent preserves inputs and runs same-key performances together" $ withHarness \harness -> do first <- liftEffect makeGate second <- liftEffect makeGate + let + task = Halo.concurrent Search work + idleTask = Halo.concurrent Save work liftEffect do - dispatch harness.runtime (StartTask Every 1 first) - dispatch harness.runtime (StartTask Every 2 second) + dispatch harness.runtime (Perform task { value: 1, gate: first }) + dispatch harness.runtime (Perform task { value: 2, gate: second }) - void $ await "first Every task launch" first.launched - void $ await "second Every task launch" second.launched - void $ await "first Every task start" first.started - void $ await "second Every task start" second.started + void $ await "first concurrent task start" first.started + void $ await "second concurrent task start" second.started awaitCounts harness { running: 2, queued: 0 } + activity <- liftEffect $ Ref.read harness.activity + Halo.activity task activity `shouldEqual` { running: 2, queued: 0 } + Halo.activity idleTask activity `shouldEqual` { running: 0, queued: 0 } release second release first - void $ await "first Every task completion" first.settled - void $ await "second Every task completion" second.settled - awaitCounts harness { running: 0, queued: 0 } - + void $ await "first concurrent task completion" first.settled + void $ await "second concurrent task completion" second.settled state <- liftEffect $ Ref.read harness.state Array.sort state `shouldEqual` [ 1, 2 ] - it "Restartable cancels and commit-fences the previous keyed task" $ withHarness \harness -> do + it "perform_ submits a unit-input task" $ withHarness \harness -> do + gate <- liftEffect makeGate + let task = Halo.concurrent Save \_ -> runGate 7 gate + liftEffect $ dispatch harness.runtime (PerformUnit task gate) + + void $ await "unit task start" gate.started + release gate + void $ await "unit task completion" gate.settled + state <- liftEffect $ Ref.read harness.state + state `shouldEqual` [ 7 ] + + it "restartable cancels and commit-fences previous work" $ withHarness \harness -> do stale <- liftEffect makeGate current <- liftEffect makeGate + let task = Halo.restartable Search work - liftEffect $ dispatch harness.runtime (StartTask (Restartable Search) 1 stale) - void $ await "stale Restartable task start" stale.started - liftEffect $ dispatch harness.runtime (StartTask (Restartable Search) 2 current) - - void $ await "stale Restartable task cancellation" stale.settled - void $ await "replacement Restartable task start" current.started - awaitCounts harness { running: 1, queued: 0 } + liftEffect $ dispatch harness.runtime (Perform task { value: 1, gate: stale }) + void $ await "stale restartable task start" stale.started + liftEffect $ dispatch harness.runtime (Perform task { value: 2, gate: current }) + void $ await "stale restartable task cancellation" stale.settled + void $ await "replacement restartable task start" current.started release current - void $ await "replacement Restartable task completion" current.settled - awaitCounts harness { running: 0, queued: 0 } - + void $ await "replacement restartable task completion" current.settled state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 2 ] - it "Drop ignores a new task while the key is busy" $ withHarness \harness -> do + it "drop ignores new input while its key is busy" $ withHarness \harness -> do running <- liftEffect makeGate dropped <- liftEffect makeGate + let task = Halo.drop Save work - liftEffect $ dispatch harness.runtime (StartTask (Drop Save) 1 running) - void $ await "Drop task start" running.started - liftEffect $ dispatch harness.runtime (StartTask (Drop Save) 2 dropped) - void $ await "dropped task launching action completion" dropped.launched + liftEffect $ dispatch harness.runtime (Perform task { value: 1, gate: running }) + void $ await "drop task start" running.started + liftEffect $ dispatch harness.runtime (Perform task { value: 2, gate: dropped }) + void $ await "dropped submission" dropped.launched awaitCounts harness { running: 1, queued: 0 } shouldNotHaveStarted dropped release running - void $ await "Drop task completion" running.settled - awaitCounts harness { running: 0, queued: 0 } + void $ await "drop task completion" running.settled shouldNotHaveStarted dropped - state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1 ] - it "Enqueue runs all keyed tasks FIFO, one at a time" $ withHarness \harness -> do + it "enqueue preserves every queued payload FIFO" $ withHarness \harness -> do first <- liftEffect makeGate second <- liftEffect makeGate third <- liftEffect makeGate + let task = Halo.enqueue Save work liftEffect do - dispatch harness.runtime (StartTask (Enqueue Save) 1 first) - dispatch harness.runtime (StartTask (Enqueue Save) 2 second) - dispatch harness.runtime (StartTask (Enqueue Save) 3 third) + dispatch harness.runtime (Perform task { value: 1, gate: first }) + dispatch harness.runtime (Perform task { value: 2, gate: second }) + dispatch harness.runtime (Perform task { value: 3, gate: third }) - void $ await "first Enqueue task start" first.started - void $ await "second Enqueue task submission" second.launched - void $ await "third Enqueue task submission" third.launched + void $ await "first enqueue task start" first.started awaitCounts harness { running: 1, queued: 2 } shouldNotHaveStarted second shouldNotHaveStarted third release first - void $ await "second Enqueue task start" second.started - awaitCounts harness { running: 1, queued: 1 } + void $ await "second enqueue task start" second.started release second - void $ await "third Enqueue task start" third.started - awaitCounts harness { running: 1, queued: 0 } + void $ await "third enqueue task start" third.started release third - void $ await "third Enqueue task completion" third.settled - awaitCounts harness { running: 0, queued: 0 } - + void $ await "third enqueue task completion" third.settled state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1, 2, 3 ] - it "KeepLatest retains only the newest queued task" $ withHarness \harness -> do + it "keepLatest retains only the newest queued payload" $ withHarness \harness -> do first <- liftEffect makeGate discarded <- liftEffect makeGate latest <- liftEffect makeGate + let task = Halo.keepLatest Search work liftEffect do - dispatch harness.runtime (StartTask (KeepLatest Search) 1 first) - dispatch harness.runtime (StartTask (KeepLatest Search) 2 discarded) - dispatch harness.runtime (StartTask (KeepLatest Search) 3 latest) + dispatch harness.runtime (Perform task { value: 1, gate: first }) + dispatch harness.runtime (Perform task { value: 2, gate: discarded }) + dispatch harness.runtime (Perform task { value: 3, gate: latest }) - void $ await "current KeepLatest task start" first.started - void $ await "discarded task submission" discarded.launched - void $ await "latest task submission" latest.launched + void $ await "current keepLatest task start" first.started awaitCounts harness { running: 1, queued: 1 } - shouldNotHaveStarted discarded - shouldNotHaveStarted latest - release first - void $ await "latest KeepLatest task start" latest.started + void $ await "latest keepLatest task start" latest.started shouldNotHaveStarted discarded release latest - void $ await "latest KeepLatest task completion" latest.settled - awaitCounts harness { running: 0, queued: 0 } - + void $ await "latest keepLatest task completion" latest.settled state <- liftEffect $ Ref.read harness.state state `shouldEqual` [ 1, 3 ] - it "cancels running and queued tasks for a key" $ withHarness \harness -> do + it "cancel uses task identity to cancel running and queued work" $ withHarness \harness -> do running <- liftEffect makeGate queued <- liftEffect makeGate cancelled <- liftEffect EffectAVar.empty + let task = Halo.enqueue Search work liftEffect do - dispatch harness.runtime (StartTask (Enqueue Search) 1 running) - dispatch harness.runtime (StartTask (Enqueue Search) 2 queued) - void $ await "task before keyed cancellation" running.started - void $ await "queued task submission" queued.launched + dispatch harness.runtime (Perform task { value: 1, gate: running }) + dispatch harness.runtime (Perform task { value: 2, gate: queued }) + void $ await "task before cancellation" running.started awaitCounts harness { running: 1, queued: 1 } - liftEffect $ dispatch harness.runtime (CancelTask Search cancelled) - void $ await "keyed cancellation action" cancelled - void $ await "running keyed task cancellation" running.settled + liftEffect $ dispatch harness.runtime (Cancel task cancelled) + void $ await "task cancellation action" cancelled + void $ await "running task cancellation" running.settled awaitCounts harness { running: 0, queued: 0 } shouldNotHaveStarted queued - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [] - - it "reports explicit keyed task activity for rendering" $ withHarness \harness -> do - running <- liftEffect makeGate - queued <- liftEffect makeGate + it "same-key definitions with the same strategy intentionally share a slot" $ withHarness \harness -> do + first <- liftEffect makeGate + second <- liftEffect makeGate + let + firstDefinition = Halo.enqueue Search work + secondDefinition = Halo.enqueue Search work liftEffect do - dispatch harness.runtime (StartTask (Enqueue Search) 1 running) - dispatch harness.runtime (StartTask (Enqueue Search) 2 queued) - void $ await "keyed activity task start" running.started + dispatch harness.runtime (Perform firstDefinition { value: 1, gate: first }) + dispatch harness.runtime (Perform secondDefinition { value: 2, gate: second }) + void $ await "shared slot first task" first.started awaitCounts harness { running: 1, queued: 1 } - activity <- liftEffect $ Ref.read harness.activity - activityFor Search activity `shouldEqual` { running: 1, queued: 1 } - activityFor Save activity `shouldEqual` { running: 0, queued: 0 } + Halo.activity firstDefinition activity `shouldEqual` Halo.activity secondDefinition activity + release first + void $ await "shared slot second task" second.started + release second + void $ await "shared slot completion" second.settled + it "rejects a conflicting same-key strategy through onError" $ withHarness \harness -> do + running <- liftEffect makeGate + rejected <- liftEffect makeGate + let + established = Halo.enqueue Search work + conflicting = Halo.restartable Search work + + liftEffect do + dispatch harness.runtime (Perform established { value: 1, gate: running }) + dispatch harness.runtime (Perform conflicting { value: 2, gate: rejected }) + void $ await "established task start" running.started + void $ await "configuration error" harness.errorRaised + shouldNotHaveStarted rejected + errors <- liftEffect $ Ref.read harness.errors + errors `shouldEqual` + [ "task configuration Search: Task key was already defined as enqueue and cannot also be defined as restartable" ] release running - void $ await "queued keyed activity task start" queued.started - release queued - void $ await "queued keyed activity task completion" queued.settled - awaitCounts harness { running: 0, queued: 0 } + + it "remembers a key's strategy across deactivate and reactivate" $ withHarness \harness -> do + first <- liftEffect makeGate + conflictingGate <- liftEffect makeGate + let + established = Halo.concurrent Search work + conflicting = Halo.drop Search work + + liftEffect $ dispatch harness.runtime (Perform established { value: 1, gate: first }) + void $ await "task before deactivation" first.started + liftEffect $ deactivate harness.runtime + void $ await "deactivated task cancellation" first.settled + liftEffect do + activate harness.runtime + dispatch harness.runtime (Perform conflicting { value: 2, gate: conflictingGate }) + void $ await "remembered configuration error" harness.errorRaised + shouldNotHaveStarted conflictingGate diff --git a/test/Test/Halo/ScopeHandlerSpec.purs b/test/Test/Halo/ScopeHandlerSpec.purs index 209fab3..57cd426 100644 --- a/test/Test/Halo/ScopeHandlerSpec.purs +++ b/test/Test/Halo/ScopeHandlerSpec.purs @@ -12,10 +12,10 @@ import Effect.AVar (AVar) import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Ref as Ref +import React.Halo as Halo import React.Halo.Handlers (defaultHandlers) -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, props, startTask, syncSpec, updateProps) -import React.Halo.Internal.Types (TaskPolicy(..)) -import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, props, syncSpec, updateProps) +import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness, work) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) @@ -26,9 +26,10 @@ spec = describe "scope and handlers" do queued <- liftEffect makeGate ignored <- liftEffect makeGate + let task = Halo.enqueue Save work liftEffect do - dispatch harness.runtime (StartTask (Enqueue Save) 1 running) - dispatch harness.runtime (StartTask (Enqueue Save) 2 queued) + dispatch harness.runtime (Perform task { value: 1, gate: running }) + dispatch harness.runtime (Perform task { value: 2, gate: queued }) void $ await "running task before deactivation" running.started awaitCounts harness { running: 1, queued: 1 } @@ -37,7 +38,8 @@ spec = describe "scope and handlers" do awaitCounts harness { running: 0, queued: 0 } shouldNotHaveStarted queued - liftEffect $ dispatch harness.runtime (StartTask Every 3 ignored) + let ignoredTask = Halo.concurrent Search work + liftEffect $ dispatch harness.runtime (Perform ignoredTask { value: 3, gate: ignored }) shouldNotHaveStarted ignored state <- liftEffect $ Ref.read harness.state state `shouldEqual` [] @@ -45,7 +47,7 @@ spec = describe "scope and handlers" do reactivated <- liftEffect makeGate liftEffect do activate harness.runtime - dispatch harness.runtime (StartTask Every 4 reactivated) + dispatch harness.runtime (Perform ignoredTask { value: 4, gate: reactivated }) void $ await "task start after reactivation" reactivated.started release reactivated void $ await "task completion after reactivation" reactivated.settled @@ -195,17 +197,8 @@ spec = describe "scope and handlers" do , spec: { handlers: defaultHandlers { onAction = case _ of - ParentTask parent child -> startTask (Restartable unit) do - void $ fork do - runIntGate child - modify_ (_ + 100) - liftAff $ void $ AVar.take child.started - runIntGate parent - modify_ (_ + 1) - ReplacementTask gate completed -> startTask (Restartable unit) do - runIntGate gate - modify_ (_ + 10) - liftAff $ void $ AVar.tryPut unit completed + ParentTask parent child -> Halo.perform parentTask (ParentWork parent child) + ReplacementTask gate completed -> Halo.perform parentTask (ReplacementWork gate completed) } , onError: \_ _ -> pure unit } @@ -268,6 +261,24 @@ data ParentAction = ParentTask Gate Gate | ReplacementTask Gate (AVar Unit) +data ParentInput + = ParentWork Gate Gate + | ReplacementWork Gate (AVar Unit) + +parentTask :: Halo.Task Unit Int ParentAction Unit ParentInput +parentTask = Halo.restartable unit case _ of + ParentWork parent child -> do + void $ fork do + runIntGate child + modify_ (_ + 100) + liftAff $ void $ AVar.take child.started + runIntGate parent + modify_ (_ + 1) + ReplacementWork gate completed -> do + runIntGate gate + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit completed + runIntGate :: forall props action key. Gate -> HaloM props Int action key Unit runIntGate gate = do liftAff $ Aff.finally diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index 1502f3e..f9fabd1 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -13,9 +13,10 @@ import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Exception as Exception import Effect.Ref as Ref +import React.Halo as Halo import React.Halo.Handlers (Handlers, defaultHandlers) import React.Halo.Internal.Runtime (activate, createRuntime, deactivate, dispatch, subscribe, syncSpec, unsubscribe) -import React.Halo.Internal.Types (ErrorContext(..), SubscriptionId, TaskPolicy(..)) +import React.Halo.Internal.Types (ErrorContext(..), SubscriptionId) import React.Halo.Subscription (Emitter, makeEmitter) import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, handlers, makeGate, withHarness) import Test.Spec (Spec, describe, it) @@ -153,15 +154,22 @@ spec = describe "subscriptions and errors" do newErrors <- liftEffect $ Ref.read replacementErrors newErrors `shouldEqual` [ "replacement action: boom" ] - it "routes an explicit task failure with TaskError" $ withHarness \harness -> do + it "routes an explicit task failure with its task key" $ withHarness \harness -> do gate <- liftEffect makeGate - liftEffect $ dispatch harness.runtime (TaskBoom (Restartable Save) gate) + let + failingTask = Halo.restartable Save \_ -> + liftAff $ Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + Aff.throwError (Aff.error "task boom") + liftEffect $ dispatch harness.runtime (PerformUnit failingTask gate) void $ await "failing task start" gate.started void $ await "task error handler" harness.errorRaised awaitCounts harness { running: 0, queued: 0 } errors <- liftEffect $ Ref.read harness.errors - errors `shouldEqual` [ "task: task boom" ] + errors `shouldEqual` [ "task Save: task boom" ] data SubscriptionAction = Start (Emitter SubscriptionAction) (AVar Unit) From b44015efaf99425ea3d60cba610a30494a7901f2 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 18:27:29 +0900 Subject: [PATCH 05/16] Clarify the public Halo vocabulary --- README.md | 2 +- docs/guide.md | 10 +++++----- docs/migration-v4.md | 4 ++-- docs/reference.md | 20 ++++++++++---------- src/React/Halo.purs | 6 +++--- src/React/Halo/Handlers.purs | 4 ++-- src/React/Halo/Hook.purs | 6 +++--- src/React/Halo/Internal/Runtime.purs | 18 +++++++++--------- src/React/Halo/Internal/Types.purs | 8 ++++---- test/Test/Halo/DocExamples.purs | 11 ++++++++++- test/Test/Halo/Helpers.purs | 4 ++-- test/Test/Halo/ScopeHandlerSpec.purs | 4 ++-- 12 files changed, 53 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 6a46939..62dd5d7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ derive instance ordTaskKey :: Ord TaskKey loadGreetingTask :: Halo.Task Props State Action TaskKey Unit loadGreetingTask = Halo.restartable GreetingRequest \_ -> do modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.props + Props { loadGreeting } <- Halo.getProps outcome <- liftAff $ attempt loadGreeting modify_ _ { loading = false diff --git a/docs/guide.md b/docs/guide.md index a85176f..c1ed60e 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -87,7 +87,7 @@ Halo calls this for each React effect activation. Development StrictMode can run ### `onPropsChange previousProps` -Halo runs this when the props reference changes. The argument is the previous props; read current props with `Halo.props`. Halo always selects callbacks from the latest spec for new work. +Halo runs this when the props reference changes. The argument is the previous props; read current props with `Halo.getProps`. Halo always selects callbacks from the latest spec for new work. ### `onAction action` @@ -95,7 +95,7 @@ Halo starts this when rendering code dispatches or a subscription emits an actio ## Work with state and props -`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use ordinary `get`, `put`, and `modify_`. `Halo.props` reads the latest props. +`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use ordinary `get`, `put`, and `modify_`. `Halo.getProps` reads the latest props. When work becomes stale through replacement, cancellation, or deactivation, later Halo state operations cannot commit. Foreign effects that already occurred cannot be reversed. Capture render- or action-associated values as task input instead of relying on later props: @@ -103,7 +103,7 @@ When work becomes stale through replacement, cancellation, or deactivation, late submitTask = Halo.drop SubmitRequest submit onAction Submit = do - { form } <- Halo.props + { form } <- Halo.getProps Halo.perform submitTask form ``` @@ -160,7 +160,7 @@ Definitions sharing a key share cancellation. If a task cancels its own key, it ```purescript let searchCounts = Halo.activity searchTask halo.activity - totalCounts = Halo.activityTotals halo.activity + totalCounts = Halo.totalActivity halo.activity ``` Each count is `{ running, queued }`. Every task is keyed, so totals are the sum of all keyed slots. Definitions sharing a key report the same counts. @@ -177,7 +177,7 @@ events = Halo.makeEmitter \emit -> do pure (source.remove listener) ``` -`subscribe events` registers an action source in the current activation scope; `unsubscribe id` removes it early. Manual unsubscription removes tracking before cleanup runs. Deactivation attempts every tracked cleanup even when one throws, then reports failures as `DeactivationError`. +`subscribe events` registers an action source in the current activation scope; `subscribeWithId (\id -> emitterFor id)` exposes the allocated ID during emitter setup; and `unsubscribe id` removes either form early. Manual unsubscription removes tracking before cleanup runs. Deactivation attempts every tracked cleanup even when one throws, then reports failures as `DeactivationError`. Emitters broadcast without consuming-queue or backpressure semantics. Each emission dispatches an action; the handler may perform a task with the appropriate pressure strategy. diff --git a/docs/migration-v4.md b/docs/migration-v4.md index 95e53f1..c01d73d 100644 --- a/docs/migration-v4.md +++ b/docs/migration-v4.md @@ -109,7 +109,7 @@ Activity lookup also takes the task: ```purescript searchCounts = Halo.activity searchTask halo.activity -totalCounts = Halo.activityTotals halo.activity +totalCounts = Halo.totalActivity halo.activity ``` Every task, including `concurrent`, is keyed. Replace direct key lookup with the task-based helper. @@ -176,7 +176,7 @@ events = Halo.makeEmitter \emit -> do pure (source.remove listener) ``` -`subscribe`, `subscribe'`, and `unsubscribe` remain. Manual cleanup is removed from tracking before it runs; scope cleanup failures are isolated and reported through `DeactivationError`. +`subscribe` and `unsubscribe` remain; use the semantic name `subscribeWithId` when emitter setup needs the allocated ID. Manual cleanup is removed from tracking before it runs; scope cleanup failures are isolated and reported through `DeactivationError`. ## Behavior changes to verify diff --git a/docs/reference.md b/docs/reference.md index 6987ec4..3c6fba8 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -23,8 +23,8 @@ The parameters are component props, Halo state, dispatched actions, application ```purescript type Handlers props state action key = { onActivate :: HaloM props state action key Unit - , onAction :: action -> HaloM props state action key Unit , onPropsChange :: props -> HaloM props state action key Unit + , onAction :: action -> HaloM props state action key Unit } defaultHandlers :: forall props state action key. Handlers props state action key @@ -33,8 +33,8 @@ defaultHandlers :: forall props state action key. Handlers props state action ke `defaultHandlers` ignores every callback. Handlers are active-scope-owned, concurrent, commit-fenced, and excluded from task activity. - `onActivate` runs for every React effect activation. +- `onPropsChange previousProps` starts when the props reference changes; use `getProps` for current props. - `onAction` starts for each action dispatched while active. -- `onPropsChange previousProps` starts when the props reference changes; use `props` for current props. ## Task definitions @@ -117,19 +117,19 @@ activity -> Activity key -> TaskCounts -activityTotals :: Activity key -> TaskCounts +totalActivity :: Activity key -> TaskCounts emptyActivity :: Activity key ``` -`activity task snapshot` reports the task key's slot; same-key definitions report the same counts. `activityTotals` sums every slot. Activity counts only `perform`/`perform_` submissions, not handlers, structured children, or subscriptions. +`activity task snapshot` reports the task key's slot; same-key definitions report the same counts. `totalActivity` sums every slot. Activity counts only `perform`/`perform_` submissions, not handlers, structured children, or subscriptions. ## State and props ```purescript -props :: HaloM props state action key props +getProps :: HaloM props state action key props ``` -Use `MonadState` operations for state. `props` returns the latest component props. State mutation and capability acquisition are commit-fenced when the current owner becomes stale. +Use `MonadState` operations for state. `getProps` returns the latest component props. State mutation and capability acquisition are commit-fenced when the current owner becomes stale. ## Structured children @@ -157,7 +157,7 @@ subscribe => Emitter action -> HaloM props state action key SubscriptionId -subscribe' +subscribeWithId :: Ord key => (SubscriptionId -> Emitter action) -> HaloM props state action key SubscriptionId @@ -174,11 +174,11 @@ Emitter registration receives a receiver and returns cleanup. Emissions dispatch ```purescript data ErrorContext props action key = ActivationError - | DeactivationError | PropsChangeError props | ActionError action | TaskError key | TaskConfigurationError key + | DeactivationError onError :: ErrorContext props action key -> Error -> Effect Unit ``` @@ -195,7 +195,7 @@ type HookSpec props state action key = , props :: props } -type HaloHook state action key = +type HaloResult state action key = { activity :: Activity key , dispatch :: action -> Effect Unit , state :: state @@ -204,7 +204,7 @@ type HaloHook state action key = useHalo :: Ord key => HookSpec props state action key - -> Hook (UseHalo props state action key) (HaloHook state action key) + -> Hook (UseHalo props state action key) (HaloResult state action key) ``` The hook synchronizes the latest handlers and React callbacks. Cleanup deactivates its scope; StrictMode reactivation creates a fresh scope while retaining the runtime's key-strategy validation. diff --git a/src/React/Halo.purs b/src/React/Halo.purs index 3ffb070..699e367 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -4,8 +4,8 @@ module React.Halo import React.Halo.Component (ComponentSpec, component) as Exports import React.Halo.Handlers (Handlers, defaultHandlers) as Exports -import React.Halo.Hook (HaloHook, HookSpec, UseHalo(..), useHalo) as Exports -import React.Halo.Internal.Runtime (HaloM, fork, kill, props, subscribe, subscribe', unsubscribe) as Exports -import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, SubscriptionId, TaskCounts, activityTotals, emptyActivity) as Exports +import React.Halo.Hook (HaloResult, HookSpec, UseHalo, useHalo) as Exports +import React.Halo.Internal.Runtime (HaloM, fork, getProps, kill, subscribe, subscribeWithId, unsubscribe) as Exports +import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, SubscriptionId, TaskCounts, emptyActivity, totalActivity) as Exports import React.Halo.Subscription (Emitter, makeEmitter) as Exports import React.Halo.Task (Task, activity, cancel, concurrent, drop, enqueue, keepLatest, perform, perform_, restartable) as Exports diff --git a/src/React/Halo/Handlers.purs b/src/React/Halo/Handlers.purs index 6069ddd..caf7ce8 100644 --- a/src/React/Halo/Handlers.purs +++ b/src/React/Halo/Handlers.purs @@ -11,7 +11,7 @@ import React.Halo.Internal.Runtime (Handlers) as Runtime -- | -- | `onActivate` may run again after React replays an effect setup. It is not an -- | exactly-once mount callback. `onPropsChange` receives the previous props; --- | use `React.Halo.props` to read the current props. `onAction` starts +-- | use `React.Halo.getProps` to read the current props. `onAction` starts -- | immediately for every dispatched action. type Handlers props state action key = Runtime.Handlers props state action key @@ -20,6 +20,6 @@ type Handlers props state action key = Runtime.Handlers props state action key defaultHandlers :: forall props state action key. Handlers props state action key defaultHandlers = { onActivate: pure unit - , onAction: \_ -> pure unit , onPropsChange: \_ -> pure unit + , onAction: \_ -> pure unit } diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index 4ac0f7d..1a6d1f3 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -1,5 +1,5 @@ module React.Halo.Hook - ( HaloHook + ( HaloResult , HookSpec , UseHalo(..) , useHalo @@ -30,7 +30,7 @@ type HookSpec props state action key = } -- | State, action dispatch, and explicit task activity exposed to rendering code. -type HaloHook state action key = +type HaloResult state action key = { activity :: Activity key , dispatch :: action -> Effect Unit , state :: state @@ -59,7 +59,7 @@ useHalo :: forall props state action key . Ord key => HookSpec props state action key - -> Hook (UseHalo props state action key) (HaloHook state action key) + -> Hook (UseHalo props state action key) (HaloResult state action key) useHalo { props, initialState, handlers, onError } = React.coerceHook React.do state /\ setState <- React.useState' initialState diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index 21a11f1..a64f633 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -9,10 +9,10 @@ module React.Halo.Internal.Runtime , dispatch , fork , kill + , getProps , performTask - , props , subscribe - , subscribe' + , subscribeWithId , syncSpec , unsubscribe , updateProps @@ -64,8 +64,8 @@ derive newtype instance monadAffHaloM :: MonadAff (HaloM props state action key) -- | scope-owned computations; only work submitted with `perform` enters the task scheduler. type Handlers props state action key = { onActivate :: HaloM props state action key Unit - , onAction :: action -> HaloM props state action key Unit , onPropsChange :: props -> HaloM props state action key Unit + , onAction :: action -> HaloM props state action key Unit } type RuntimeSpec props state action key = @@ -281,8 +281,8 @@ dispatchToScope runtime@(Runtime state) scope action = do startHandler runtime scope (ActionError action) (spec.handlers.onAction action) -- | Read the latest component props. -props :: forall props state action key. HaloM props state action key props -props = HaloM do +getProps :: forall props state action key. HaloM props state action key props +getProps = HaloM do execution <- ask let Runtime runtime = execution.runtime liftEffect $ Ref.read runtime.props @@ -334,15 +334,15 @@ subscribe . Ord key => Emitter action -> HaloM props state action key SubscriptionId -subscribe = subscribe' <<< const +subscribe = subscribeWithId <<< const --- | Like `subscribe`, but provide the allocated identifier to the emitter. -subscribe' +-- | Subscribe while providing the allocated identifier to the emitter. +subscribeWithId :: forall props state action key . Ord key => (SubscriptionId -> Emitter action) -> HaloM props state action key SubscriptionId -subscribe' makeEmitter = HaloM do +subscribeWithId makeEmitter = HaloM do execution <- ask liftEffect do sid <- SubscriptionId <$> fresh execution.runtime diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index b96111d..bd38305 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -5,7 +5,7 @@ module React.Halo.Internal.Types , SubscriptionId(..) , TaskCounts , activityAtKey - , activityTotals + , totalActivity , emptyActivity ) where @@ -21,11 +21,11 @@ import Data.Maybe (Maybe(..)) -- | configuration conflicts carry the affected task key. data ErrorContext props action key = ActivationError - | DeactivationError | PropsChangeError props | ActionError action | TaskError key | TaskConfigurationError key + | DeactivationError -- | Counts of explicit scheduled tasks. Handler and structured-child execution -- | is intentionally excluded. @@ -53,8 +53,8 @@ emptyActivity = Activity } -- | Read total running and queued explicit task counts. -activityTotals :: forall key. Activity key -> TaskCounts -activityTotals (Activity activity) = activity.total +totalActivity :: forall key. Activity key -> TaskCounts +totalActivity (Activity activity) = activity.total -- | Internal keyed lookup used by the abstract Task API. activityAtKey :: forall key. Ord key => key -> Activity key -> TaskCounts diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index b97effb..0c1d970 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -31,7 +31,7 @@ derive instance ordTaskKey :: Ord TaskKey loadGreetingTask :: Halo.Task Props State Action TaskKey Unit loadGreetingTask = Halo.restartable GreetingRequest \_ -> do modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.props + Props { loadGreeting } <- Halo.getProps outcome <- liftAff $ attempt loadGreeting modify_ _ { loading = false @@ -124,3 +124,12 @@ simpleHandlers = Halo.defaultHandlers { onActivate = pure unit , onAction = \InitializeData -> pure unit } + +simpleSubscription :: Halo.HaloM Unit Unit SimpleAction Unit Unit +simpleSubscription = void $ Halo.subscribeWithId \_ -> simpleEmitter + +totalWorkflowActivity :: Halo.Activity WorkflowTask -> Halo.TaskCounts +totalWorkflowActivity = Halo.totalActivity + +readSimpleState :: Halo.HaloResult Unit SimpleAction Unit -> Unit +readSimpleState = _.state diff --git a/test/Test/Halo/Helpers.purs b/test/Test/Halo/Helpers.purs index e424349..3627113 100644 --- a/test/Test/Halo/Helpers.purs +++ b/test/Test/Halo/Helpers.purs @@ -39,7 +39,7 @@ import Effect.Ref as Ref import React.Halo as Halo import React.Halo.Handlers (Handlers, defaultHandlers) import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, fork) -import React.Halo.Internal.Types (Activity, ErrorContext(..), TaskCounts, activityTotals, emptyActivity) +import React.Halo.Internal.Types (Activity, ErrorContext(..), TaskCounts, emptyActivity, totalActivity) import Test.Spec.Assertions (fail, shouldEqual) data Key = Search | Save @@ -182,7 +182,7 @@ awaitCounts :: Harness -> TaskCounts -> Aff Unit awaitCounts harness expected = go 20 where go remaining = do - actual <- activityTotals <$> liftEffect (Ref.read harness.activity) + actual <- totalActivity <$> liftEffect (Ref.read harness.activity) if actual == expected then pure unit else if remaining <= 0 then fail $ "Expected activity " <> show expected <> " but got " <> show actual diff --git a/test/Test/Halo/ScopeHandlerSpec.purs b/test/Test/Halo/ScopeHandlerSpec.purs index 57cd426..ddfc0b7 100644 --- a/test/Test/Halo/ScopeHandlerSpec.purs +++ b/test/Test/Halo/ScopeHandlerSpec.purs @@ -14,7 +14,7 @@ import Effect.Class (liftEffect) import Effect.Ref as Ref import React.Halo as Halo import React.Halo.Handlers (defaultHandlers) -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, props, syncSpec, updateProps) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, getProps, syncSpec, updateProps) import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness, work) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) @@ -105,7 +105,7 @@ spec = describe "scope and handlers" do , spec: { handlers: defaultHandlers { onPropsChange = \previous -> do - current <- props + current <- getProps liftAff $ void $ AVar.tryPut (Tuple previous current) changed } , onError: \_ _ -> pure unit From 5aa1d5748d859fee50ad2b555501f5b1e1ac6d8f Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 19:26:40 +0900 Subject: [PATCH 06/16] Clarify props and consumer setup --- README.md | 18 +++++++++++++----- docs/guide.md | 4 +++- docs/reference.md | 2 +- src/React/Halo/Component.purs | 3 +++ test/Test/Halo/DocExamples.purs | 4 ++-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 62dd5d7..dd75799 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,16 @@ Halo v4 targets PureScript 0.15.16 and Spago 1.0.4. It is not published yet; the ```yaml package: dependencies: - - react-basic-dom # only needed by this README's renderer + - aff + - console + - either + - exceptions + - maybe + - prelude + - react-basic-dom + - react-basic-hooks - react-halo + - transformers workspace: extraPackages: @@ -33,10 +41,10 @@ workspace: After v4 is published: ```console -spago install react-halo react-basic-dom +spago install aff console either exceptions maybe prelude react-basic-dom react-basic-hooks react-halo transformers ``` -Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. +This list is intentionally complete for the pasted quick-start module under Spago's pedantic dependency check; an existing React application will already declare several of these packages. `react-basic-dom` is required by the example renderer, not by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. ## Quick start: replace a stale request @@ -59,7 +67,7 @@ import React.Basic.DOM.Events (capture_) import React.Basic.Hooks (Component) import React.Halo as Halo -newtype Props = Props { loadGreeting :: Aff String } +type Props = { loadGreeting :: Aff String } type State = { loading :: Boolean @@ -76,7 +84,7 @@ derive instance ordTaskKey :: Ord TaskKey loadGreetingTask :: Halo.Task Props State Action TaskKey Unit loadGreetingTask = Halo.restartable GreetingRequest \_ -> do modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.getProps + { loadGreeting } <- Halo.getProps outcome <- liftAff $ attempt loadGreeting modify_ _ { loading = false diff --git a/docs/guide.md b/docs/guide.md index c1ed60e..58f3dff 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -206,7 +206,9 @@ Replacement, cancellation, and deactivation synchronously mark old owners stale, ## Choose `component` or `useHalo` -Use `Halo.component` when Halo owns the whole component. Its renderer receives props, state, dispatch, and activity. Use `Halo.useHalo` when composing with other hooks: +Use `Halo.component` when Halo owns the whole component. Its renderer receives props, state, dispatch, and activity. `ComponentSpec.initialState` receives the initial props once per mount; synchronize later prop changes in `onPropsChange` rather than expecting state to be reinitialized. + +Use `Halo.useHalo` when composing with other hooks: ```purescript halo <- Halo.useHalo diff --git a/docs/reference.md b/docs/reference.md index 3c6fba8..32dbfe6 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -232,4 +232,4 @@ component -> Component props ``` -Use `component` when Halo owns the whole component. Use `useHalo` when other React hooks share the render function. +Use `component` when Halo owns the whole component. `initialState` receives the initial props once per mount; later prop changes call `handlers.onPropsChange` and do not reinitialize state. Use `useHalo` when other React hooks share the render function. diff --git a/src/React/Halo/Component.purs b/src/React/Halo/Component.purs index b75a278..d8aca3e 100644 --- a/src/React/Halo/Component.purs +++ b/src/React/Halo/Component.purs @@ -14,6 +14,9 @@ import React.Halo.Hook (useHalo) import React.Halo.Internal.Types (Activity, ErrorContext) -- | Complete configuration for a Halo-owned React component. +-- | +-- | `initialState` receives the initial props once per mount. Later prop changes +-- | run `handlers.onPropsChange` and do not recreate state. type ComponentSpec props state action key = { handlers :: Handlers props state action key , initialState :: props -> state diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index 0c1d970..8e9024b 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -14,7 +14,7 @@ import React.Basic.DOM.Events (capture_) import React.Basic.Hooks (Component) import React.Halo as Halo -newtype Props = Props { loadGreeting :: Aff String } +type Props = { loadGreeting :: Aff String } type State = { loading :: Boolean @@ -31,7 +31,7 @@ derive instance ordTaskKey :: Ord TaskKey loadGreetingTask :: Halo.Task Props State Action TaskKey Unit loadGreetingTask = Halo.restartable GreetingRequest \_ -> do modify_ _ { loading = true, result = Nothing } - Props { loadGreeting } <- Halo.getProps + { loadGreeting } <- Halo.getProps outcome <- liftAff $ attempt loadGreeting modify_ _ { loading = false From e182ccca0aa8504b65b22d6bd9d8c75471fbe10d Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 21:12:01 +0900 Subject: [PATCH 07/16] Restore application-monad Halo runtime --- README.md | 167 +++-- docs/guide.md | 254 +++----- docs/migration-v4.md | 194 ------ docs/reference.md | 222 +++---- package.json | 2 +- spago.lock | 3 +- spago.yaml | 3 +- src/React/Halo.purs | 5 +- src/React/Halo/Component.purs | 33 +- src/React/Halo/Handlers.purs | 8 +- src/React/Halo/Hook.purs | 59 +- src/React/Halo/Internal/Runtime.purs | 761 ++++++++-------------- src/React/Halo/Internal/Task.purs | 41 -- src/React/Halo/Internal/Types.purs | 68 +- src/React/Halo/Task.purs | 106 --- test/Main.purs | 4 +- test/Test/Halo/DocExamples.purs | 204 +++--- test/Test/Halo/Helpers.purs | 167 +---- test/Test/Halo/RuntimeSpec.purs | 125 ++++ test/Test/Halo/SchedulerSpec.purs | 229 ------- test/Test/Halo/ScopeHandlerSpec.purs | 336 +++++----- test/Test/Halo/SubscriptionErrorSpec.purs | 205 ++++-- 22 files changed, 1174 insertions(+), 2022 deletions(-) delete mode 100644 docs/migration-v4.md delete mode 100644 src/React/Halo/Internal/Task.purs delete mode 100644 src/React/Halo/Task.purs create mode 100644 test/Test/Halo/RuntimeSpec.purs delete mode 100644 test/Test/Halo/SchedulerSpec.purs diff --git a/README.md b/README.md index dd75799..a02f4dd 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # React Halo -Halo gives a PureScript React component one typed action handler plus reusable, component-scoped tasks. It is for UI workflows where plain hooks become hard to coordinate: replace stale searches, prevent overlapping saves, preserve upload order, or retain only the newest pending refresh. +Halo gives a PureScript React component one typed action handler, component state, and a safe boundary for application effects. Define UI interactions with an action ADT, lift your application monad into `HaloM`, and supply an interpreter from that monad to `Aff` when the component or hook is created. -For one request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. Halo earns its place when actions, state transitions, cancellation, and task concurrency need one coherent owner. +Each active React effect owns its handlers, component forks, and subscriptions. Deactivation cancels that work, and work that has been killed or deactivated cannot commit Halo state. -## Mental model +For one request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. Halo is useful when actions, shared state transitions, application logic, and cancellable component processes need one coherent owner. -Halo separates three kinds of work: +## Mental model -1. **Handlers** react to activation, prop changes, and dispatched actions. They start immediately, belong to the active React scope, and do not count as task activity. -2. **Tasks** are first-class definitions created with `concurrent`, `restartable`, `drop`, `enqueue`, or `keepLatest`. A definition binds its identity, scheduling strategy, and input-driven implementation. `perform` submits work that can outlive its caller and drives `Activity`. -3. **Structured children** are created with `fork`. A child belongs to its current handler or task and is cancelled when that parent finishes. +1. **Actions** are values in your UI action ADT. Rendering code calls `dispatch :: action -> Effect Unit`; Halo starts the action handler in the active component scope. +2. **Application effects** remain in your application monad, commonly `ReaderT AppEnv Aff`. Use the standard transformer `lift` inside `HaloM`. The interpreter supplied to `component` or `useHalo` runs those effects in Halo-owned `Aff` fibers. +3. **Forks** are component-owned processes. A fork may outlive the handler that started it, can be killed by its `ForkId`, and is cancelled when the React scope deactivates. -An action is an event, not an implicit task. The action handler decides whether to update state, perform or cancel a task, subscribe to events, or combine those operations. +Halo does not provide global state, server caching, or a separate process runtime. ## Try the unreleased v4 @@ -23,8 +23,10 @@ package: dependencies: - aff - console + - effect - either - exceptions + - foldable-traversable - maybe - prelude - react-basic-dom @@ -41,90 +43,111 @@ workspace: After v4 is published: ```console -spago install aff console either exceptions maybe prelude react-basic-dom react-basic-hooks react-halo transformers +spago install aff console effect either exceptions foldable-traversable maybe prelude react-basic-dom react-basic-hooks react-halo transformers ``` -This list is intentionally complete for the pasted quick-start module under Spago's pedantic dependency check; an existing React application will already declare several of these packages. `react-basic-dom` is required by the example renderer, not by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. +This list is complete for the quick-start shape under Spago's pedantic dependency check; an existing React application will already declare several packages. `react-basic-dom` is required by the renderer, not by Halo. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. -## Quick start: replace a stale request +## Quick start -Define the request once as a restartable task. Each click dispatches an action immediately; `perform_` then fences and cancels prior work for `GreetingRequest` before starting the new request. +Define the application monad and its runtime interpreter: ```purescript -module Example.LoadButton where - -import Prelude - -import Control.Monad.State (modify_) -import Data.Either (Either(..)) -import Data.Maybe (Maybe(..)) -import Effect.Aff (Aff, attempt) -import Effect.Aff.Class (liftAff) -import Effect.Class.Console as Console -import Effect.Exception (message) -import React.Basic.DOM as R -import React.Basic.DOM.Events (capture_) -import React.Basic.Hooks (Component) -import React.Halo as Halo +newtype AppM a = AppM (ReaderT Env Aff a) + +derive newtype instance functorAppM :: Functor AppM +derive newtype instance applyAppM :: Apply AppM +derive newtype instance applicativeAppM :: Applicative AppM +derive newtype instance bindAppM :: Bind AppM +derive newtype instance monadAppM :: Monad AppM +derive newtype instance monadEffectAppM :: MonadEffect AppM +derive newtype instance monadAffAppM :: MonadAff AppM + +runAppM :: Env -> AppM ~> Aff +runAppM env (AppM program) = runReaderT program env +``` -type Props = { loadGreeting :: Aff String } +Use an action ADT for interactions and keep cancellation identity in component state: +```purescript type State = - { loading :: Boolean - , result :: Maybe (Either String String) + { fiber :: Maybe Halo.ForkId + , loading :: Boolean + , result :: Maybe String + } + +data Action = Load | Cancel + +type UI a = Halo.HaloM Props State Action AppM a + +handlers :: Halo.Handlers Props State Action AppM +handlers = Halo.defaultHandlers + { onAction = case _ of + Load -> do + previous <- gets _.fiber + traverse_ Halo.kill previous + fiber <- Halo.fork do + modify_ _ { loading = true, result = Nothing } + result <- lift loadGreeting + modify_ _ { loading = false, result = Just result } + modify_ _ { fiber = Just fiber } + + Cancel -> do + previous <- gets _.fiber + traverse_ Halo.kill previous + modify_ _ { fiber = Nothing, loading = false } } +``` + +`lift` is `Control.Monad.Trans.Class.lift`. The `AppM` value runs through the interpreter captured when that handler or fork started. A new render may supply a new interpreter for later roots without changing one already running. -data Action = Load - -data TaskKey = GreetingRequest - -derive instance eqTaskKey :: Eq TaskKey -derive instance ordTaskKey :: Ord TaskKey - -loadGreetingTask :: Halo.Task Props State Action TaskKey Unit -loadGreetingTask = Halo.restartable GreetingRequest \_ -> do - modify_ _ { loading = true, result = Nothing } - { loadGreeting } <- Halo.getProps - outcome <- liftAff $ attempt loadGreeting - modify_ _ - { loading = false - , result = Just $ case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting - } - -loadButton :: Component Props -loadButton = Halo.component "LoadButton" - { initialState: \_ -> { loading: false, result: Nothing } - , handlers: Halo.defaultHandlers - { onAction = \Load -> Halo.perform_ loadGreetingTask } - , onError: \_ error -> - Console.error $ "Unexpected Halo failure: " <> message error - , render: \{ state, dispatch, activity } -> - let counts = Halo.activity loadGreetingTask activity - in R.div_ - [ R.button +Create the component at the application boundary: + +```purescript +loadButton :: Env -> Component Props +loadButton env = Halo.component "LoadButton" (runAppM env) + { initialState: \_ -> + { fiber: Nothing, loading: false, result: Nothing } + , handlers + , onError: \context error -> + Console.error $ showContext context <> ": " <> message error + , render: \{ props, state, dispatch } -> + R.div_ + [ R.text props.title + , R.button { onClick: capture_ (dispatch Load) - , children: - [ R.text if counts.running > 0 then "Restart load" else "Load" ] + , children: [ R.text if state.loading then "Restart" else "Load" ] + } + , R.button + { onClick: capture_ (dispatch Cancel) + , children: [ R.text "Cancel" ] } - , R.text $ case state.result of - Nothing -> if state.loading then "Loading…" else "Not loaded" - Just (Left error) -> error - Just (Right greeting) -> greeting ] } ``` -The task catches an expected domain failure and stores it in state. Unexpected failures that escape a handler or task go to `onError` with an `ErrorContext`. +`initialState` receives the initial props once per mount. Later prop changes call `handlers.onPropsChange`; they do not recreate state. + +Use the hook form when Halo shares a component with other hooks: + +```purescript +halo <- Halo.useHalo (runAppM env) + { props + , initialState + , handlers + , onError + } + +-- halo.state +-- halo.dispatch +``` ## Learn and reference -- [Guide](docs/guide.md): handlers, task definitions, scheduling, cancellation, activity, subscriptions, activation, patterns, and troubleshooting. -- [API reference](docs/reference.md): public types and operations with exact semantics. -- [v3 to v4 migration](docs/migration-v4.md): breaking changes and a practical conversion sequence. +- [Guide](docs/guide.md): application monads, handlers, component ownership, cancellation, parallelism, subscriptions, and errors. +- [API reference](docs/reference.md): public types and exact runtime semantics. -The important documentation examples compile in `test/Test/Halo/DocExamples.purs`. +The documentation examples are compile-checked in `test/Test/Halo/DocExamples.purs`. ## Development @@ -136,4 +159,4 @@ npm test npx spago docs ``` -The deterministic runtime tests model React's effect setup-cleanup-setup sequence directly. A DOM mounting test is intentionally omitted because this library's npm manifest contains only the pinned PureScript compiler and Spago; the hook uses the tested runtime boundary, and the component examples are compile-checked. +The deterministic runtime tests model React's setup-cleanup-setup sequence directly. A DOM mounting test is intentionally omitted because the package manifest contains only the pinned PureScript compiler and Spago; the hook uses the tested runtime boundary, and component examples are compile-checked. diff --git a/docs/guide.md b/docs/guide.md index 58f3dff..591fc5f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,171 +1,146 @@ # Halo v4 guide -This guide explains Halo's action and first-class task model. Start with the [README quick start](../README.md); use the [API reference](reference.md) for exact signatures. +Halo combines a typed UI action handler with component state and a runtime boundary for your application monad. This guide starts from the [README quick start](../README.md); use the [API reference](reference.md) for exact signatures. -## Choose Halo for coordinated component workflows +## Keep application logic in `AppM` -Prefer ordinary React hooks when one asynchronous result follows render dependencies. Halo is useful when a component has an event protocol and several operations must share state and cancellation rules—for example, latest-request-wins search, duplicate-save prevention, ordered per-file uploads, or autosave that retains only the newest pending value. - -Halo does not provide global state, server caching, a process/saga runtime, or server-side rendering machinery. - -## Think in handlers, tasks, and structured children - -### Handlers respond to events - -Rendering code calls `dispatch :: action -> Effect Unit`. Halo immediately starts `handlers.onAction action` in the active component scope. Actions are not queued, deduplicated, or automatically treated as tasks. +Most applications already have a monad that carries services or configuration over `Aff`: ```purescript -onAction = case _ of - NameChanged name -> modify_ _ { name = name } - SearchSubmitted query -> Halo.perform searchTask query - SearchCleared -> Halo.cancel searchTask -``` - -Handlers are scope-owned and commit-fenced after deactivation, but their execution is not shown in `Activity`. +newtype AppM a = AppM (ReaderT AppEnv Aff a) -### Task definitions bind identity, strategy, and work +runAppM :: AppEnv -> AppM ~> Aff +runAppM env (AppM program) = runReaderT program env +``` -A task is a reusable value: +Halo restores that monad as the fourth `HaloM` parameter: ```purescript -searchTask = Halo.restartable SearchRequest \query -> do - results <- liftAff $ fetchResults query - modify_ _ { query = query, results = results } +HaloM props state action AppM result ``` -Its type is `Task props state action key input`. The `key` identifies the scheduler slot; `input` is supplied separately on each `perform`. The smart constructor fixes the scheduling strategy so a call site cannot accidentally change concurrency behavior. - -`perform task input` submits work and returns immediately. `perform_ task` is the `Unit`-input convenience form. Submitted work belongs to the active component scope, not to the handler or task that submitted it, so it can outlive successful caller completion. It may read props, update state, create structured children, or perform another task. - -Define stable tasks near the workflow they implement and perform them from handlers. A function may return a task when the key itself is dynamic: +Use the standard transformer operation to run application logic: ```purescript -uploadTask fileId = Halo.enqueue (Upload fileId) \chunk -> upload chunk +import Control.Monad.Trans.Class (lift) -onAction (UploadChunk fileId chunk) = - Halo.perform (uploadTask fileId) chunk +loadAccount :: UI Unit +loadAccount = do + account <- lift Account.load + modify_ _ { account = Just account } ``` -### Structured children stay with their parent - -`fork child` starts concurrent work owned by the current handler or task. Unlike performed work, a forked child is cancelled when its parent finishes normally. +The interpreter is explicit at the React boundary: ```purescript -refreshTask = Halo.restartable Refresh \_ -> do - left <- Halo.fork loadLeftPane - right <- Halo.fork loadRightPane - waitUntilReady - Halo.kill left - Halo.kill right +Halo.component "Account" (runAppM env) spec +Halo.useHalo (runAppM env) hookSpec ``` -Returning immediately after `fork` cancels the child. Use `perform` for component-scoped work that must survive the current handler; use `fork` for subwork whose lifetime must not exceed its parent. +A handler or fork keeps the interpreter with which it started. If a later render supplies another interpreter, only new roots use it. Do not implement an interpreter by detaching work with `launchAff_`: Halo can only own and cancel the `Aff` returned by the interpreter. -## Configure handlers +## Handle an action ADT -```purescript -type Handlers props state action key = - { onActivate :: HaloM props state action key Unit - , onPropsChange :: props -> HaloM props state action key Unit - , onAction :: action -> HaloM props state action key Unit - } -``` - -Start with `defaultHandlers` and replace the fields you need: +Rendering code receives `dispatch :: action -> Effect Unit`. Each dispatch starts `handlers.onAction action` in the current component scope: ```purescript +data Action + = NameChanged String + | Save + | CancelSave + handlers = Halo.defaultHandlers - { onActivate = initializeView - , onPropsChange = \previous -> synchronize previous - , onAction = handleAction + { onAction = case _ of + NameChanged name -> modify_ _ { name = name } + Save -> save + CancelSave -> cancelSave } ``` -### `onActivate` - -Halo calls this for each React effect activation. Development StrictMode can run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable. Activation work and tasks it performs are cancelled on deactivation. - -### `onPropsChange previousProps` - -Halo runs this when the props reference changes. The argument is the previous props; read current props with `Halo.getProps`. Halo always selects callbacks from the latest spec for new work. - -### `onAction action` - -Halo starts this when rendering code dispatches or a subscription emits an action. Dispatch while inactive is ignored. Long handler waits remain cancellable but are invisible to task activity; use a task when scheduling or renderable progress matters. +Actions are concurrent by default, matching the event-driven model: one long-running action does not block later dispatches. Work is still cancelled when the React scope deactivates. Use a component-owned fork when another action needs an ID with which to cancel a process. ## Work with state and props -`HaloM props state action key` has `MonadState state`, `MonadEffect`, and `MonadAff` instances. Use ordinary `get`, `put`, and `modify_`. `Halo.getProps` reads the latest props. +`HaloM` has `MonadState state`. Use normal `get`, `put`, `gets`, `modify`, and `modify_` operations. -When work becomes stale through replacement, cancellation, or deactivation, later Halo state operations cannot commit. Foreign effects that already occurred cannot be reversed. Capture render- or action-associated values as task input instead of relying on later props: +`Halo.getProps` reads the latest props. `onPropsChange` receives the previous props, so both sides of a synchronization are available: ```purescript -submitTask = Halo.drop SubmitRequest submit - -onAction Submit = do - { form } <- Halo.getProps - Halo.perform submitTask form +onPropsChange = \previous -> do + current <- Halo.getProps + synchronize previous current ``` -## Choose a scheduling strategy +State commits are fenced. After a handler or fork is killed, or after its activation deactivates, later Halo state operations can still compute their return value but cannot commit a new state or call React's state setter. -Every task has a key, including concurrent tasks. +Capture values associated with an action before starting work when they must not change during that work. `getProps` intentionally reads current props rather than a render snapshot. -### `concurrent key implementation` +## Start and kill component processes -Every performance starts immediately, including multiple performances for the same key. Use this for independent bounded work such as metrics. A high-rate producer can create unbounded concurrency. +`Halo.fork child` starts a process owned by the current React activation and returns a `ForkId`. The process may outlive the handler that created it: -### `restartable key implementation` - -A performance synchronously fences all running work for the key, discards its queue, requests cancellation, and starts the new input. Use it for search and navigation where the newest request wins. +```purescript +startSearch query = do + previous <- gets _.searchFiber + traverse_ Halo.kill previous -### `drop key implementation` + fiber <- Halo.fork do + modify_ _ { loading = true } + results <- lift (Search.run query) + modify_ _ { loading = false, results = results } -A performance starts only when the key has no running or queued work. Otherwise its input is discarded and `perform` returns normally. Use it for optional duplicate submissions. + modify_ _ { searchFiber = Just fiber } +``` -### `enqueue key implementation` +`Halo.kill id` removes the fork from component tracking, fences its state and capabilities synchronously, requests Aff cancellation, and waits for cancellation and Aff finalizers before returning. Killing an unknown or completed ID does nothing. -Every input is preserved FIFO and runs one at a time for the key. Different keys remain independent. The queue is unbounded, so bound the producer when input can exceed throughput. +Deactivation cannot wait asynchronously because React cleanup is synchronous. It fences the whole activation first, attempts every subscription cleanup, and requests cancellation of all remaining handlers and forks. Aff finalizers continue in their cancellation fibers, but they cannot commit Halo state. -### `keepLatest key implementation` +Cancellation is cooperative. It cannot retract an HTTP request, storage write, callback, or log that already happened. Design external writes for retry and idempotency where needed. -Current work may finish; only the newest queued input is retained. Intermediate queued inputs are discarded. Use it for autosave when in-flight writes should not be cancelled. +## Run independent work in parallel -## Understand task identity and shared keys +`HaloM` has a direct `Parallel` instance with abstract counterpart `HaloAp`. Branches share the same root, scope, and interpreter snapshot: -The task value carries a key, but scheduling coordination is by key—not JavaScript object identity. Two definitions with the same key and strategy intentionally share one scheduler slot, cancellation boundary, and activity count. This supports separately named operations that must serialize together. +```purescript +loadDashboard = do + Tuple profile feed <- sequential ado + profile <- parallel (lift Profile.load) + feed <- parallel (lift Feed.load) + in Tuple profile feed -A key's first performed task establishes its strategy for the entire component runtime lifetime. Performing another definition with the same key and a different strategy is rejected: no work starts, and `onError` receives `TaskConfigurationError key` with an error naming both strategies. The association remains across StrictMode deactivate/reactivate cycles. This catches accidental key reuse while permitting deliberate same-strategy sharing. + modify_ _ { profile = profile, feed = feed } +``` -Use distinct keys for independent work. Do not treat task input as identity: changing input creates another performance of the same task. +Prefer parallel application reads followed by one Halo state commit. Concurrent Halo state writes have nondeterministic ordering; a later commit can overwrite an earlier one. -## Cancel a task +Parallel work is lexical: the combined computation waits for its branches. Use `fork` only when work must continue independently of the launching handler or needs explicit cancellation by ID. -`cancel task` synchronously fences every running performance and discards every queued input for the task's key, requests fiber cancellation, publishes activity, and returns. +## Configure lifecycle handlers ```purescript -onAction = case _ of - SearchChanged query -> Halo.perform searchTask query - SearchCleared -> do - Halo.cancel searchTask - modify_ _ { results = [] } +type Handlers props state action m = + { onActivate :: HaloM props state action m Unit + , onPropsChange :: props -> HaloM props state action m Unit + , onAction :: action -> HaloM props state action m Unit + } ``` -Definitions sharing a key share cancellation. If a task cancels its own key, it fences itself and all keyed siblings. +Start with `defaultHandlers` and replace only what the component needs. -## Render task activity +### `onActivate` -`component` renderers and `useHalo` return `Activity key`: +Halo calls this for every React effect activation. Development StrictMode can run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable. Work from a prior activation is fenced and cancelled before a new activation becomes current. -```purescript -let - searchCounts = Halo.activity searchTask halo.activity - totalCounts = Halo.totalActivity halo.activity -``` +### `onPropsChange previousProps` + +Halo starts this when the props reference changes. Read current props with `getProps`. New prop-change roots use the latest handlers and interpreter supplied by the hook. + +### `onAction action` -Each count is `{ running, queued }`. Every task is keyed, so totals are the sum of all keyed slots. Definitions sharing a key report the same counts. +Halo starts one root for every action dispatched while active, including actions emitted by subscriptions. Dispatch while inactive is ignored. -Activity includes only performed tasks. It excludes activation, prop-change, and action handlers, structured `fork` children, and subscription cleanup. +There is no asynchronous deactivation callback. Use subscriptions, Aff finalizers, or an external resource owner with explicit cleanup semantics. ## Subscribe to custom emitters @@ -177,16 +152,18 @@ events = Halo.makeEmitter \emit -> do pure (source.remove listener) ``` -`subscribe events` registers an action source in the current activation scope; `subscribeWithId (\id -> emitterFor id)` exposes the allocated ID during emitter setup; and `unsubscribe id` removes either form early. Manual unsubscription removes tracking before cleanup runs. Deactivation attempts every tracked cleanup even when one throws, then reports failures as `DeactivationError`. +`subscribe events` registers an action source in the current activation. `subscribeWithId (\id -> emitterFor id)` exposes the allocated `SubscriptionId` during registration. `unsubscribe id` removes tracking before running cleanup. -Emitters broadcast without consuming-queue or backpressure semantics. Each emission dispatches an action; the handler may perform a task with the appropriate pressure strategy. +Deactivation attempts every tracked cleanup even when one throws. Cleanup failures are reported as `DeactivationError` only after Halo has requested cleanup for the rest of the scope. A callback retained by a faulty source remains bound to its original activation and cannot dispatch into a later StrictMode reactivation. + +Emitters broadcast actions. They do not provide backpressure, consuming queues, or scheduling policies. ## Handle unexpected errors Every spec supplies: ```purescript -onError :: ErrorContext props action key -> Error -> Effect Unit +onError :: ErrorContext props action -> Error -> Effect Unit ``` Contexts are: @@ -194,64 +171,13 @@ Contexts are: - `ActivationError` for `onActivate`; - `PropsChangeError previousProps` for `onPropsChange`; - `ActionError action` for `onAction`; -- `TaskError key` for performed task failure; -- `TaskConfigurationError key` for conflicting same-key strategies; and +- `ForkError id` for a component-owned fork; and - `DeactivationError` for throwing subscription cleanup. -Catch expected domain failures inside the task and put them in state or dispatch a domain action. Let unexpected failures reach `onError`. Halo-initiated cancellation is suppressed. - -## Understand cancellation limits - -Replacement, cancellation, and deactivation synchronously mark old owners stale, which blocks later Halo state commits and new Halo-owned capabilities, then request `Aff` cancellation. Cancellation is cooperative. It cannot retract an HTTP request, storage write, analytics event, or foreign callback already performed. Design external operations for retry, ordering, and idempotency where needed. +Halo selects the latest `onError` callback when an unexpected failure is reported. Expected domain failures belong in application values or Halo state. Cancellation initiated by Halo is suppressed because the root has already been fenced. ## Choose `component` or `useHalo` -Use `Halo.component` when Halo owns the whole component. Its renderer receives props, state, dispatch, and activity. `ComponentSpec.initialState` receives the initial props once per mount; synchronize later prop changes in `onPropsChange` rather than expecting state to be reinitialized. - -Use `Halo.useHalo` when composing with other hooks: - -```purescript -halo <- Halo.useHalo - { props - , initialState - , handlers - , onError - } -``` - -Read `halo.state`, call `halo.dispatch`, and pass `halo.activity` to a task's `Halo.activity` helper. - -## Common patterns - -```purescript -searchTask = Halo.restartable SearchRequest search -saveTask = Halo.drop SaveRequest saveCurrentForm -uploadTask fileId = Halo.enqueue (Upload fileId) uploadChunk -autosaveTask = Halo.keepLatest AutosaveRequest saveDraft -metricTask = Halo.concurrent Metrics recordMetric - -onAction = case _ of - SearchChanged query -> Halo.perform searchTask query - SaveClicked -> Halo.perform_ saveTask - UploadChunk fileId chunk -> Halo.perform (uploadTask fileId) chunk - DraftChanged draft -> Halo.perform autosaveTask draft - MetricRecorded metric -> Halo.perform metricTask metric -``` - -## Troubleshooting and footguns - -**My fork stops immediately.** Its parent returned. Perform a component-scoped task, or keep the parent alive while it owns the child. - -**Activity is zero while work is running.** The work is probably in a handler or structured child. Only `perform`/`perform_` submissions count. - -**A performed task was rejected with `TaskConfigurationError`.** Two definitions reuse a key with different strategies. Give independent work distinct keys or make deliberately shared definitions use one strategy. - -**A dropped input did not run cleanup or report an error.** `drop` never starts the implementation when busy. Put only optional work behind it. - -**My queue keeps growing.** `enqueue` is unbounded. Limit input, batch it, or use `keepLatest`/`drop`. - -**A cancelled request still reached the server.** Halo fences component commits and requests cancellation; it cannot undo external effects. - -**Initialization ran twice in development.** StrictMode replayed activation. Make `onActivate` replay-safe. +Use `Halo.component` when Halo owns the whole component. The renderer receives `{ props, state, dispatch }`. `initialState` receives initial props once per mount; synchronize later changes in `onPropsChange`. -**An emitter overwhelms the component.** Reduce events at the source or perform a task with an appropriate pressure strategy. +Use `Halo.useHalo` when other React hooks share the render function. It accepts the same interpreter and returns `{ state, dispatch }`. diff --git a/docs/migration-v4.md b/docs/migration-v4.md deleted file mode 100644 index c01d73d..0000000 --- a/docs/migration-v4.md +++ /dev/null @@ -1,194 +0,0 @@ -# Migrate from Halo v3 to v4 - -Halo v4 is an unreleased breaking redesign. It replaces the Free/FreeAp evaluator and implicit action effects with a direct scoped runtime, named handlers, and first-class tasks. There are no compatibility aliases. - -## Why the model changed - -In v3, `eval` combined lifecycle events and actions, and asynchronous action work was conventional rather than explicit. It was difficult to tell whether an action was an event, a long-running task, or both, and concurrency ownership was easy to obscure. - -In v4: - -- handlers respond to activation, prop changes, and actions; -- reusable task definitions bind identity, scheduling strategy, and implementation; -- `perform` explicitly starts component-scoped work; -- `fork` is explicitly parent-scoped; and -- activity counts performed tasks only. - -## Migration sequence - -### 1. Change `HaloM` - -Replace: - -```purescript -HaloM props state action m a -``` - -with: - -```purescript -HaloM props state action key a -``` - -Choose an application task-key type with an `Ord` instance. Halo now runs directly on `Aff`; remove the custom base monad parameter, `hoist`, `HaloAp`, and Free/FreeAp-specific code. Use `liftAff` for asynchronous effects. - -Task input does not become another `HaloM` parameter. It is generic on each `Task` value. - -### 2. Replace `eval` with `handlers` - -Replace lifecycle pattern matching: - -```purescript -eval = case _ of - Initialize -> initialize - Update previous -> synchronize previous - Action action -> handleAction action - Finalize -> finalize -``` - -with: - -```purescript -handlers = Halo.defaultHandlers - { onActivate = initialize - , onPropsChange = synchronize - , onAction = handleAction - } -``` - -There is no public `Lifecycle`, `EvalSpec`, `mkEval`, or `defaultEval` in v4. - -`onActivate` is repeatable under React StrictMode. There is no asynchronous deactivation handler: React cleanup is synchronous. Use subscription cleanup, `Aff` finalizers, or an external resource owner. - -### 3. Define long-running operations as tasks - -Create a key type, then define each task once: - -```purescript -data TaskKey = SearchRequest | SaveRequest - -derive instance eqTaskKey :: Eq TaskKey -derive instance ordTaskKey :: Ord TaskKey - -searchTask :: Halo.Task Props State Action TaskKey String -searchTask = Halo.restartable SearchRequest \query -> do - results <- liftAff $ search query - modify_ _ { results = results } - -saveTask :: Halo.Task Props State Action TaskKey Unit -saveTask = Halo.drop SaveRequest \_ -> saveCurrentForm -``` - -The available constructors are `concurrent`, `restartable`, `drop`, `enqueue`, and `keepLatest`. Each takes a key and an input-driven implementation. Strategy is part of the definition and cannot vary at performance sites. - -A key's first performance establishes its strategy for the component runtime lifetime. Deliberate same-key, same-strategy definitions share a slot. Conflicting same-key strategies are rejected through `TaskConfigurationError key`. - -### 4. Perform tasks from actions - -Replace direct long-running action work or any action-to-policy table with: - -```purescript -onAction = case _ of - SearchChanged query -> Halo.perform searchTask query - SaveClicked -> Halo.perform_ saveTask -``` - -An action is no longer implicitly a task. Some actions only update state; others may perform multiple tasks. - -### 5. Replace keyed cancellation and activity lookup - -Cancellation now takes the task definition: - -```purescript -Halo.cancel searchTask -``` - -This fences running work and discards queued work for the task's key. Same-key definitions share cancellation. - -Activity lookup also takes the task: - -```purescript -searchCounts = Halo.activity searchTask halo.activity -totalCounts = Halo.totalActivity halo.activity -``` - -Every task, including `concurrent`, is keyed. Replace direct key lookup with the task-based helper. - -### 6. Update the error handler - -Change: - -```purescript -onError :: ErrorContext props action -> Error -> Effect Unit -``` - -into: - -```purescript -onError :: ErrorContext props action key -> Error -> Effect Unit -``` - -Handle: - -- `ActivationError`; -- `DeactivationError` for subscription cleanup; -- `PropsChangeError previousProps`; -- `ActionError action`; -- `TaskError key`; and -- `TaskConfigurationError key`. - -Expected request failures still belong in domain state or actions. - -### 7. Update hook and component specs - -Remove `eval` and `schedule`; add `handlers`: - -```purescript -halo <- Halo.useHalo - { props - , initialState - , handlers - , onError - } -``` - -`useHalo` returns `state`, `dispatch`, and `activity`. `Halo.component` renderers receive `{ props, state, dispatch, activity }`; the old `send` field is now `dispatch`. - -### 8. Revisit every `fork` - -A v4 `fork` is a structured child. It is cancelled when its creating handler or task finishes. If old code expected a fork to survive handler completion, make it a task and call `perform`: - -```purescript -backgroundSync = Halo.restartable BackgroundSync \_ -> synchronize - -onAction StartSync = Halo.perform_ backgroundSync -``` - -Use `fork` only for concurrency owned by a parent that remains alive. - -### 9. Replace Halogen emitters - -Halo v4 has its own emitter type: - -```purescript -events = Halo.makeEmitter \emit -> do - listener <- source.listen emit - pure (source.remove listener) -``` - -`subscribe` and `unsubscribe` remain; use the semantic name `subscribeWithId` when emitter setup needs the allocated ID. Manual cleanup is removed from tracking before it runs; scope cleanup failures are isolated and reported through `DeactivationError`. - -## Behavior changes to verify - -Before completing a migration, verify: - -- `onActivate` is safe to replay; -- task keys are stable and distinct where work is independent; -- definitions sharing a key use one intentional strategy; -- `drop` inputs are genuinely optional; -- `enqueue` producers cannot grow an unbounded queue unexpectedly; -- `cancel task` is used when UI state must clear work without replacement; -- activity-dependent UI expects performed tasks only; -- structured children do not need to outlive parents; -- expected failures are modeled in state rather than logged as unexpected errors; and -- external writes remain correct even when local cancellation cannot undo them. diff --git a/docs/reference.md b/docs/reference.md index 32dbfe6..c9740f5 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -6,219 +6,176 @@ Import the intentional public surface from `React.Halo`: import React.Halo as Halo ``` -Runtime constructors, task representation, and scheduling strategies are internal. +Runtime constructors and ownership records are internal. `ForkId` and `SubscriptionId` constructors are hidden. ## Core computation ```purescript -HaloM props state action key a +HaloM props state action m a +HaloAp props state action m a ``` -`HaloM` runs on `Aff` in a private scoped environment. It has `Functor`, `Apply`, `Applicative`, `Bind`, `Monad`, `MonadState state`, `MonadEffect`, and `MonadAff` instances. +`HaloM` is the sequential component computation. Its parameters are current props, Halo state, dispatched actions, the application's base monad, and the result. -The parameters are component props, Halo state, dispatched actions, application task keys, and the result. Task input is generic on each `Task`; it is deliberately not another `HaloM` parameter. - -## Handlers - -```purescript -type Handlers props state action key = - { onActivate :: HaloM props state action key Unit - , onPropsChange :: props -> HaloM props state action key Unit - , onAction :: action -> HaloM props state action key Unit - } - -defaultHandlers :: forall props state action key. Handlers props state action key -``` - -`defaultHandlers` ignores every callback. Handlers are active-scope-owned, concurrent, commit-fenced, and excluded from task activity. - -- `onActivate` runs for every React effect activation. -- `onPropsChange previousProps` starts when the props reference changes; use `getProps` for current props. -- `onAction` starts for each action dispatched while active. - -## Task definitions +It has unconditional `Functor`, `Apply`, `Applicative`, `Bind`, `Monad`, and `MonadState state` instances. It is a `MonadTrans` in its `m` parameter: ```purescript -Task props state action key input +lift :: Monad m => m a -> HaloM props state action m a ``` -`Task` is abstract. It binds a key, a strategy, and an `input -> HaloM ... Unit` implementation. +The following capabilities are lifted through `m`, rather than executed directly in Halo's private `Aff`: ```purescript -concurrent - :: key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input - -restartable - :: key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input - -drop - :: key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input - -enqueue - :: key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input - -keepLatest - :: key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input +MonadEffect m => MonadEffect (HaloM props state action m) +MonadAff m => MonadAff (HaloM props state action m) +MonadAsk environment m => MonadAsk environment (HaloM props state action m) +MonadTell output m => MonadTell output (HaloM props state action m) +MonadThrow error m => MonadThrow error (HaloM props state action m) ``` -- `concurrent`: every performance starts immediately, including same-key work. -- `restartable`: fences/cancels running work, discards queued work, then starts the new input. -- `drop`: discards the new input while the key is busy. -- `enqueue`: preserves every input FIFO and runs one at a time. -- `keepLatest`: lets running work finish and retains only the newest queued input. - -The first performed definition for a key fixes that key's strategy for the component runtime lifetime. Same key plus same strategy shares a slot. Same key plus a different strategy is rejected through `onError` as `TaskConfigurationError key`, including across deactivate/reactivate. - -## Performance and cancellation +`HaloAp` is the abstract parallel applicative counterpart: ```purescript -perform - :: Ord key - => Task props state action key input - -> input - -> HaloM props state action key Unit - -perform_ - :: Ord key - => Task props state action key Unit - -> HaloM props state action key Unit - -cancel - :: Ord key - => Task props state action key input - -> HaloM props state action key Unit +Parallel + (HaloAp props state action m) + (HaloM props state action m) ``` -`perform` and `perform_` submit component-scoped work and return without waiting. The submitted work outlives successful completion of its caller and is cancelled on scope deactivation. - -`cancel` synchronously fences running work and discards queued work for the task's key, requests cancellation, updates activity, and returns. All definitions sharing the key share this boundary. +Parallel branches share their root, component scope, and `m ~> Aff` interpreter snapshot. Concurrent Halo state writes have nondeterministic ordering; combine independent results before committing state when possible. -## Activity +## Handlers ```purescript -type TaskCounts = - { running :: Int - , queued :: Int +type Handlers props state action m = + { onActivate :: HaloM props state action m Unit + , onPropsChange :: props -> HaloM props state action m Unit + , onAction :: action -> HaloM props state action m Unit } -activity - :: Ord key - => Task props state action key input - -> Activity key - -> TaskCounts - -totalActivity :: Activity key -> TaskCounts -emptyActivity :: Activity key +defaultHandlers + :: forall props state action m + . Handlers props state action m ``` -`activity task snapshot` reports the task key's slot; same-key definitions report the same counts. `totalActivity` sums every slot. Activity counts only `perform`/`perform_` submissions, not handlers, structured children, or subscriptions. +Handlers are component-activation-owned roots: + +- `onActivate` runs for each React effect activation and is repeatable under StrictMode. +- `onPropsChange previousProps` starts when the props reference changes. +- `onAction action` starts for each dispatch while active. + +Handlers selected for new roots come from the latest hook spec. There is no asynchronous deactivation handler. ## State and props +Use the `MonadState state` operations for Halo state. + ```purescript -getProps :: HaloM props state action key props +getProps + :: forall props state action m + . HaloM props state action m props ``` -Use `MonadState` operations for state. `getProps` returns the latest component props. State mutation and capability acquisition are commit-fenced when the current owner becomes stale. +`getProps` returns the latest props. State commits from a stale root are ignored. -## Structured children +## Component-owned forks ```purescript fork - :: HaloM props state action key Unit - -> HaloM props state action key ForkId + :: forall props state action m + . HaloM props state action m Unit + -> HaloM props state action m ForkId kill - :: ForkId - -> HaloM props state action key Unit + :: forall props state action m + . ForkId + -> HaloM props state action m Unit ``` -`fork` creates a concurrent child owned by the current handler or performed task. Parent completion or cancellation cancels the child. `kill` requests earlier cancellation. `ForkId` is abstract. +`fork` starts a root owned by the current React activation. It may outlive its launching handler. The child uses the launching root's interpreter snapshot, but receives an independent state/capability fence and reports unexpected failures as `ForkError id`. + +`kill` removes a tracked fork, fences its Halo state and capabilities synchronously, and then waits for Aff cancellation and finalizers. Killing an unknown or completed ID does nothing. Deactivation fences and requests cancellation of every remaining fork and handler. ## Subscriptions and emitters ```purescript makeEmitter - :: ((action -> Effect Unit) -> Effect (Effect Unit)) + :: forall action + . ((action -> Effect Unit) -> Effect (Effect Unit)) -> Emitter action subscribe - :: Ord key - => Emitter action - -> HaloM props state action key SubscriptionId + :: forall props state action m + . Emitter action + -> HaloM props state action m SubscriptionId subscribeWithId - :: Ord key - => (SubscriptionId -> Emitter action) - -> HaloM props state action key SubscriptionId + :: forall props state action m + . (SubscriptionId -> Emitter action) + -> HaloM props state action m SubscriptionId unsubscribe - :: SubscriptionId - -> HaloM props state action key Unit + :: forall props state action m + . SubscriptionId + -> HaloM props state action m Unit ``` -Emitter registration receives a receiver and returns cleanup. Emissions dispatch actions into the registering activation scope; stale callbacks cannot target a later scope. Manual unsubscribe removes tracking before cleanup. Deactivation attempts all remaining cleanup, and reports thrown cleanup as `DeactivationError` without preventing other cleanup and cancellation. +Emitter registration receives a receiver and returns its cleanup effect. Emissions dispatch into the activation that registered the receiver. A stale callback cannot target a later activation. + +Manual unsubscription removes tracking before cleanup runs. Deactivation attempts every remaining cleanup; throwing cleanup is isolated and reported as `DeactivationError` without preventing other cleanup and cancellation requests. ## Errors ```purescript -data ErrorContext props action key +data ErrorContext props action = ActivationError | PropsChangeError props | ActionError action - | TaskError key - | TaskConfigurationError key + | ForkError ForkId | DeactivationError -onError :: ErrorContext props action key -> Error -> Effect Unit +onError + :: ErrorContext props action + -> Error + -> Effect Unit ``` -Unexpected handler and task failures reach `onError`. `TaskError` identifies the task key. `TaskConfigurationError` identifies a same-key strategy conflict. `DeactivationError` reports throwing subscription cleanup. Halo suppresses cancellation errors it initiated. +Halo reports unexpected root failures through the latest `onError` callback. Cancellation initiated by Halo is suppressed. Expected domain failures should be represented in application values, actions, or state. ## Hook API ```purescript -type HookSpec props state action key = - { handlers :: Handlers props state action key +type HookSpec props state action m = + { handlers :: Handlers props state action m , initialState :: state - , onError :: ErrorContext props action key -> Error -> Effect Unit + , onError :: ErrorContext props action -> Error -> Effect Unit , props :: props } -type HaloResult state action key = - { activity :: Activity key - , dispatch :: action -> Effect Unit +type HaloResult state action = + { dispatch :: action -> Effect Unit , state :: state } useHalo - :: Ord key - => HookSpec props state action key - -> Hook (UseHalo props state action key) (HaloResult state action key) + :: forall props state action m + . (m ~> Aff) + -> HookSpec props state action m + -> Hook + (UseHalo props state action m) + (HaloResult state action) ``` -The hook synchronizes the latest handlers and React callbacks. Cleanup deactivates its scope; StrictMode reactivation creates a fresh scope while retaining the runtime's key-strategy validation. +The natural transformation is captured for each new handler or fork root. Updating the hook with another interpreter affects later roots only. Cleanup deactivates the current scope; StrictMode reactivation creates a fresh usable scope. ## Component API ```purescript -type ComponentSpec props state action key = - { handlers :: Handlers props state action key +type ComponentSpec props state action m = + { handlers :: Handlers props state action m , initialState :: props -> state - , onError :: ErrorContext props action key -> Error -> Effect Unit + , onError :: ErrorContext props action -> Error -> Effect Unit , render :: - { activity :: Activity key - , dispatch :: action -> Effect Unit + { dispatch :: action -> Effect Unit , props :: props , state :: state } @@ -226,10 +183,11 @@ type ComponentSpec props state action key = } component - :: Ord key - => String - -> ComponentSpec props state action key + :: forall props state action m + . String + -> (m ~> Aff) + -> ComponentSpec props state action m -> Component props ``` -Use `component` when Halo owns the whole component. `initialState` receives the initial props once per mount; later prop changes call `handlers.onPropsChange` and do not reinitialize state. Use `useHalo` when other React hooks share the render function. +`initialState` receives initial props once per mount. Later prop changes invoke `handlers.onPropsChange` and do not reinitialize state. Use `component` when Halo owns the whole component and `useHalo` when other hooks share the render function. diff --git a/package.json b/package.json index 062f352..921c34f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "purescript-react-halo", "version": "4.0.0", - "description": "Race-safe action and effect scheduling for PureScript React components", + "description": "Scoped action handling and application effects for PureScript React components", "scripts": { "build": "spago build", "format": "npx --yes purs-tidy@0.11.1 format-in-place src test", diff --git a/spago.lock b/spago.lock index a1f2e38..01b0719 100644 --- a/spago.lock +++ b/spago.lock @@ -6,7 +6,6 @@ "core": { "dependencies": [ "aff", - "arrays", "avar", "effect", "either", @@ -15,6 +14,7 @@ "maybe", "newtype", "ordered-collections", + "parallel", "prelude", "react-basic-hooks", "refs", @@ -28,7 +28,6 @@ "console", "control", "exceptions", - "parallel", "react-basic-dom", "spec", "spec-node" diff --git a/spago.yaml b/spago.yaml index 7cef87e..11d1a15 100644 --- a/spago.yaml +++ b/spago.yaml @@ -8,7 +8,6 @@ package: githubRepo: purescript-react-halo dependencies: - aff - - arrays - avar - effect - either @@ -17,6 +16,7 @@ package: - maybe - newtype - ordered-collections + - parallel - prelude - react-basic-hooks - refs @@ -29,7 +29,6 @@ package: - console - control - exceptions - - parallel - react-basic-dom - spec - spec-node diff --git a/src/React/Halo.purs b/src/React/Halo.purs index 699e367..62b2c28 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -5,7 +5,6 @@ module React.Halo import React.Halo.Component (ComponentSpec, component) as Exports import React.Halo.Handlers (Handlers, defaultHandlers) as Exports import React.Halo.Hook (HaloResult, HookSpec, UseHalo, useHalo) as Exports -import React.Halo.Internal.Runtime (HaloM, fork, getProps, kill, subscribe, subscribeWithId, unsubscribe) as Exports -import React.Halo.Internal.Types (Activity, ErrorContext(..), ForkId, SubscriptionId, TaskCounts, emptyActivity, totalActivity) as Exports +import React.Halo.Internal.Runtime (HaloAp, HaloM, fork, getProps, kill, subscribe, subscribeWithId, unsubscribe) as Exports +import React.Halo.Internal.Types (ErrorContext(..), ForkId, SubscriptionId) as Exports import React.Halo.Subscription (Emitter, makeEmitter) as Exports -import React.Halo.Task (Task, activity, cancel, concurrent, drop, enqueue, keepLatest, perform, perform_, restartable) as Exports diff --git a/src/React/Halo/Component.purs b/src/React/Halo/Component.purs index d8aca3e..9cfbd0d 100644 --- a/src/React/Halo/Component.purs +++ b/src/React/Halo/Component.purs @@ -6,49 +6,50 @@ module React.Halo.Component import Prelude import Effect (Effect) -import Effect.Aff (Error) +import Effect.Aff (Aff, Error) import React.Basic.Hooks (Component, JSX) import React.Basic.Hooks as React import React.Halo.Handlers (Handlers) import React.Halo.Hook (useHalo) -import React.Halo.Internal.Types (Activity, ErrorContext) +import React.Halo.Internal.Types (ErrorContext) -- | Complete configuration for a Halo-owned React component. -- | -- | `initialState` receives the initial props once per mount. Later prop changes -- | run `handlers.onPropsChange` and do not recreate state. -type ComponentSpec props state action key = - { handlers :: Handlers props state action key +type ComponentSpec props state action m = + { handlers :: Handlers props state action m , initialState :: props -> state - , onError :: ErrorContext props action key -> Error -> Effect Unit + , onError :: ErrorContext props action -> Error -> Effect Unit , render :: - { activity :: Activity key - , dispatch :: action -> Effect Unit + { dispatch :: action -> Effect Unit , props :: props , state :: state } -> JSX } --- | Build a complete React component around a Halo action and task runtime. +-- | Build a complete React component around a Halo action runtime. +-- | +-- | The natural transformation is the application boundary: it translates the +-- | component's application monad into the `Aff` fibers owned by Halo. component - :: forall props state action key - . Ord key - => String - -> ComponentSpec props state action key + :: forall props state action m + . String + -> (m ~> Aff) + -> ComponentSpec props state action m -> Component props -component name spec = +component name runInAff spec = React.component name \props -> React.do initialState <- React.useMemo unit \_ -> spec.initialState props - halo <- useHalo + halo <- useHalo runInAff { handlers: spec.handlers , initialState , onError: spec.onError , props } pure $ spec.render - { activity: halo.activity - , dispatch: halo.dispatch + { dispatch: halo.dispatch , props , state: halo.state } diff --git a/src/React/Halo/Handlers.purs b/src/React/Halo/Handlers.purs index caf7ce8..4ea6913 100644 --- a/src/React/Halo/Handlers.purs +++ b/src/React/Halo/Handlers.purs @@ -11,13 +11,13 @@ import React.Halo.Internal.Runtime (Handlers) as Runtime -- | -- | `onActivate` may run again after React replays an effect setup. It is not an -- | exactly-once mount callback. `onPropsChange` receives the previous props; --- | use `React.Halo.getProps` to read the current props. `onAction` starts --- | immediately for every dispatched action. -type Handlers props state action key = Runtime.Handlers props state action key +-- | use `React.Halo.getProps` to read the current props. `onAction` starts for +-- | every dispatched action. +type Handlers props state action m = Runtime.Handlers props state action m -- | Handlers that do nothing. Use a record update to configure only the -- | callbacks a component needs. -defaultHandlers :: forall props state action key. Handlers props state action key +defaultHandlers :: forall props state action m. Handlers props state action m defaultHandlers = { onActivate: pure unit , onPropsChange: \_ -> pure unit diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index 1a6d1f3..6910657 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -10,72 +10,64 @@ import Prelude import Data.Newtype (class Newtype) import Data.Tuple.Nested ((/\)) import Effect (Effect) -import Effect.Aff (Error) +import Effect.Aff (Aff, Error) import Effect.Unsafe (unsafePerformEffect) import React.Basic.Hooks (Hook, UseEffect, UseMemo, UseState) import React.Basic.Hooks as React import React.Halo.Handlers (Handlers) import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, syncSpec, updateProps) -import React.Halo.Internal.Types (Activity, ErrorContext, emptyActivity) +import React.Halo.Internal.Types (ErrorContext) -- | Configuration for `useHalo`. --- | --- | The application chooses `key`; only explicit tasks use it, and it needs an --- | `Ord` instance so Halo can coordinate keyed task slots. -type HookSpec props state action key = - { handlers :: Handlers props state action key +type HookSpec props state action m = + { handlers :: Handlers props state action m , initialState :: state - , onError :: ErrorContext props action key -> Error -> Effect Unit + , onError :: ErrorContext props action -> Error -> Effect Unit , props :: props } --- | State, action dispatch, and explicit task activity exposed to rendering code. -type HaloResult state action key = - { activity :: Activity key - , dispatch :: action -> Effect Unit +-- | State and action dispatch exposed to rendering code. +type HaloResult state action = + { dispatch :: action -> Effect Unit , state :: state } -newtype UseHalo props state action key hooks = UseHalo +newtype UseHalo props state action m hooks = UseHalo ( UseEffect Unit ( UseEffect Unit ( UseEffect Unit - ( UseMemo Unit (Runtime props state action key) - ( UseState (Activity key) - (UseState state hooks) - ) + ( UseMemo Unit (Runtime props state action m) + (UseState state hooks) ) ) ) ) -derive instance newtypeUseHalo :: Newtype (UseHalo props state action key hooks) _ +derive instance newtypeUseHalo :: Newtype (UseHalo props state action m hooks) _ -- | Run Halo inside a `react-basic-hooks` component. -- | --- | React effect activation owns the runtime scope. Cleanup deactivates it, and --- | a later StrictMode replay creates a fresh usable scope. +-- | The natural transformation interprets application effects in `m` into the +-- | `Aff` fibers owned by the active React scope. New roots use the latest +-- | interpreter; roots already running retain their starting snapshot. useHalo - :: forall props state action key - . Ord key - => HookSpec props state action key - -> Hook (UseHalo props state action key) (HaloResult state action key) -useHalo { props, initialState, handlers, onError } = + :: forall props state action m + . (m ~> Aff) + -> HookSpec props state action m + -> Hook (UseHalo props state action m) (HaloResult state action) +useHalo runInAff { props, initialState, handlers, onError } = React.coerceHook React.do state /\ setState <- React.useState' initialState - activity /\ setActivity <- React.useState' emptyActivity runtime <- React.useMemo unit \_ -> unsafePerformEffect $ - createRuntime - { activityUpdate: setActivity - , initialProps: props + createRuntime runInAff + { initialProps: props , initialState , spec: { handlers, onError } , stateUpdate: setState } React.useEffectAlways do - syncSpec runtime - { activityUpdate: setActivity - , spec: { handlers, onError } + syncSpec runtime runInAff + { spec: { handlers, onError } , stateUpdate: setState } pure mempty @@ -86,7 +78,6 @@ useHalo { props, initialState, handlers, onError } = updateProps runtime props pure mempty pure - { activity - , dispatch: dispatch runtime + { dispatch: dispatch runtime , state } diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index a64f633..448de33 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -1,16 +1,15 @@ module React.Halo.Internal.Runtime - ( HaloM + ( HaloAp + , HaloM , Handlers , Runtime , activate - , cancelDefinition , createRuntime , deactivate , dispatch , fork - , kill , getProps - , performTask + , kill , subscribe , subscribeWithId , syncSpec @@ -20,18 +19,21 @@ module React.Halo.Internal.Runtime import Prelude -import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.Error.Class (class MonadThrow, throwError) +import Control.Monad.Reader (ReaderT(..), class MonadAsk, ask, mapReaderT) import Control.Monad.State.Class (class MonadState) -import Data.Array as Array +import Control.Monad.Trans.Class (class MonadTrans, lift) +import Control.Monad.Writer (class MonadTell, tell) +import Control.Parallel (class Parallel, parallel, sequential) import Data.Either (Either(..)) -import Data.Foldable (and, foldl, traverse_) +import Data.Foldable (traverse_) import Data.Map (Map) import Data.Map as Map -import Data.Maybe (Maybe(..), maybe) +import Data.Maybe (Maybe(..)) import Data.Traversable (traverse) import Data.Tuple (Tuple(..)) import Effect (Effect) -import Effect.Aff (Aff, Error, Fiber) +import Effect.Aff (Aff, Error, Fiber, ParAff) import Effect.Aff as Aff import Effect.Aff.AVar as AVar import Effect.Aff.Class (class MonadAff, liftAff) @@ -40,161 +42,171 @@ import Effect.Class (class MonadEffect, liftEffect) import Effect.Exception as Exception import Effect.Ref (Ref) import Effect.Ref as Ref -import React.Halo.Internal.Task (Strategy(..), Task) -import React.Halo.Internal.Task as Task -import React.Halo.Internal.Types (Activity(..), ErrorContext(..), ForkId(..), SubscriptionId(..), emptyActivity) +import React.Halo.Internal.Types (ErrorContext(..), ForkId(..), SubscriptionId(..)) import React.Halo.Subscription (Emitter) import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) --- | The direct Halo evaluator. Its environment is intentionally private so a --- | computation can only obtain the capabilities exported by `React.Halo`. -newtype HaloM props state action key a = HaloM - (ReaderT (Execution props state action key) Aff a) - -derive newtype instance functorHaloM :: Functor (HaloM props state action key) -derive newtype instance applyHaloM :: Apply (HaloM props state action key) -derive newtype instance applicativeHaloM :: Applicative (HaloM props state action key) -derive newtype instance bindHaloM :: Bind (HaloM props state action key) -derive newtype instance monadHaloM :: Monad (HaloM props state action key) -derive newtype instance monadEffectHaloM :: MonadEffect (HaloM props state action key) -derive newtype instance monadAffHaloM :: MonadAff (HaloM props state action key) - --- | Activation, prop-change, and action callbacks. Handlers run immediately as --- | scope-owned computations; only work submitted with `perform` enters the task scheduler. -type Handlers props state action key = - { onActivate :: HaloM props state action key Unit - , onPropsChange :: props -> HaloM props state action key Unit - , onAction :: action -> HaloM props state action key Unit +-- | The Halo component computation. Application effects in `m` are translated +-- | into the current root's `Aff` fiber by the interpreter supplied to +-- | `component` or `useHalo`. +newtype HaloM props state action (m :: Type -> Type) a = HaloM + (ReaderT (Execution props state action m) Aff a) + +-- | The parallel applicative counterpart of `HaloM`. +-- | +-- | Parallel branches share the current root, component scope, and application +-- | interpreter snapshot. +newtype HaloAp props state action (m :: Type -> Type) a = HaloAp + (ReaderT (Execution props state action m) ParAff a) + +derive newtype instance functorHaloM :: Functor (HaloM props state action m) +derive newtype instance applyHaloM :: Apply (HaloM props state action m) +derive newtype instance applicativeHaloM :: Applicative (HaloM props state action m) +derive newtype instance bindHaloM :: Bind (HaloM props state action m) +derive newtype instance monadHaloM :: Monad (HaloM props state action m) + +derive newtype instance functorHaloAp :: Functor (HaloAp props state action m) +derive newtype instance applyHaloAp :: Apply (HaloAp props state action m) +derive newtype instance applicativeHaloAp :: Applicative (HaloAp props state action m) + +instance monadTransHaloM :: MonadTrans (HaloM props state action) where + lift value = HaloM $ ReaderT \execution -> + case execution.runInAff of + RunInAff run -> run value + +-- Public effect capabilities deliberately pass through the application monad. +instance monadEffectHaloM :: MonadEffect m => MonadEffect (HaloM props state action m) where + liftEffect = lift <<< liftEffect + +instance monadAffHaloM :: MonadAff m => MonadAff (HaloM props state action m) where + liftAff = lift <<< liftAff + +instance monadAskHaloM :: MonadAsk r m => MonadAsk r (HaloM props state action m) where + ask = lift ask + +instance monadTellHaloM :: MonadTell w m => MonadTell w (HaloM props state action m) where + tell = lift <<< tell + +instance monadThrowHaloM :: MonadThrow error m => MonadThrow error (HaloM props state action m) where + throwError = lift <<< throwError + +instance parallelHaloM :: Parallel (HaloAp props state action m) (HaloM props state action m) where + parallel (HaloM computation) = HaloAp (mapReaderT parallel computation) + sequential (HaloAp computation) = HaloM (mapReaderT sequential computation) + +instance monadStateHaloM :: MonadState state (HaloM props state action m) where + state updateState = HaloM $ ReaderT \execution -> + liftEffect do + current <- isCurrent execution + let Runtime runtime = execution.runtime + oldState <- Ref.read runtime.state + let Tuple result newState = updateState oldState + if current then do + unless (unsafeRefEq oldState newState) do + Ref.write newState runtime.state + update <- Ref.read runtime.stateUpdate + update newState + pure result + else pure result + +-- | Activation, prop-change, and action callbacks. Each callback is a +-- | component-scope root and may perform application effects directly. +type Handlers props state action m = + { onActivate :: HaloM props state action m Unit + , onPropsChange :: props -> HaloM props state action m Unit + , onAction :: action -> HaloM props state action m Unit } -type RuntimeSpec props state action key = - { handlers :: Handlers props state action key - , onError :: ErrorContext props action key -> Error -> Effect Unit +type RuntimeSpec props state action m = + { handlers :: Handlers props state action m + , onError :: ErrorContext props action -> Error -> Effect Unit } -newtype Runtime props state action key = Runtime - { activityUpdate :: Ref (Activity key -> Effect Unit) - , fresh :: Ref Int +newtype RunInAff m = RunInAff (m ~> Aff) + +newtype Runtime props state action m = Runtime + { fresh :: Ref Int , props :: Ref props - , scope :: Ref (Maybe (Scope props state action key)) - , spec :: Ref (RuntimeSpec props state action key) - , strategies :: Ref (Map key Strategy) + , runInAff :: Ref (RunInAff m) + , scope :: Ref (Maybe Scope) + , spec :: Ref (RuntimeSpec props state action m) , state :: Ref state , stateUpdate :: Ref (state -> Effect Unit) } -newtype Scope :: Type -> Type -> Type -> Type -> Type -newtype Scope props state action key = Scope +newtype Scope = Scope { active :: Ref Boolean + , forks :: Ref (Map ForkId Root) , generation :: Int - , roots :: Ref (Map Int (Root props state action key)) + , handlers :: Ref (Map Int Root) , subscriptions :: Ref (Map SubscriptionId (Effect Unit)) - , tasks :: Ref (Map key (TaskSlot props state action key)) - } - -type TaskRequest :: Type -> Type -> Type -> Type -> Type -type TaskRequest props state action key = - { computation :: HaloM props state action key Unit } - -data StrategyRegistration - = StrategyAccepted - | StrategyConflict Strategy - -type TaskSlot :: Type -> Type -> Type -> Type -> Type -type TaskSlot props state action key = - { queued :: Array (TaskRequest props state action key) - , running :: Map Int (Root props state action key) } -newtype Owner :: Type -> Type -> Type -> Type -> Type -newtype Owner props state action key = Owner - { alive :: Ref Boolean - , children :: Ref (Map ForkId (Root props state action key)) - , lineage :: Array (Ref Boolean) - } +newtype Owner = Owner + { alive :: Ref Boolean } -newtype Root :: Type -> Type -> Type -> Type -> Type -newtype Root props state action key = Root +newtype Root = Root { fiber :: Fiber Unit - , owner :: Owner props state action key + , owner :: Owner } -type Execution props state action key = - { context :: ErrorContext props action key - , owner :: Owner props state action key - , runtime :: Runtime props state action key - , scope :: Scope props state action key +type Execution props state action m = + { context :: ErrorContext props action + , owner :: Owner + , runInAff :: RunInAff m + , runtime :: Runtime props state action m + , scope :: Scope } -type Prepared :: Type -> Type -> Type -> Type -> Type -type Prepared props state action key = - { root :: Root props state action key +type Prepared = + { root :: Root , start :: Effect Unit } -instance monadStateHaloM :: MonadState state (HaloM props state action key) where - state f = HaloM do - execution <- ask - liftEffect do - current <- isCurrent execution - if current then do - let Runtime runtime = execution.runtime - oldState <- Ref.read runtime.state - let Tuple result newState = f oldState - unless (unsafeRefEq oldState newState) do - Ref.write newState runtime.state - update <- Ref.read runtime.stateUpdate - update newState - pure result - else do - let Runtime runtime = execution.runtime - Tuple result _ <- f <$> Ref.read runtime.state - pure result - createRuntime - :: forall props state action key - . { activityUpdate :: Activity key -> Effect Unit - , initialProps :: props + :: forall props state action m + . (m ~> Aff) + -> { initialProps :: props , initialState :: state - , spec :: RuntimeSpec props state action key + , spec :: RuntimeSpec props state action m , stateUpdate :: state -> Effect Unit } - -> Effect (Runtime props state action key) -createRuntime input = do - activityUpdate <- Ref.new input.activityUpdate + -> Effect (Runtime props state action m) +createRuntime runInAff input = do freshRef <- Ref.new 0 propsRef <- Ref.new input.initialProps + runInAffRef <- Ref.new (RunInAff runInAff) scope <- Ref.new Nothing spec <- Ref.new input.spec state <- Ref.new input.initialState - strategies <- Ref.new Map.empty stateUpdate <- Ref.new input.stateUpdate pure $ Runtime - { activityUpdate - , fresh: freshRef + { fresh: freshRef , props: propsRef + , runInAff: runInAffRef , scope , spec , state - , strategies , stateUpdate } +-- | Update render-owned callbacks and the interpreter used by roots started +-- | after this synchronization. Running roots retain their interpreter snapshot. syncSpec - :: forall props state action key - . Runtime props state action key - -> { activityUpdate :: Activity key -> Effect Unit - , spec :: RuntimeSpec props state action key + :: forall props state action m + . Runtime props state action m + -> (m ~> Aff) + -> { spec :: RuntimeSpec props state action m , stateUpdate :: state -> Effect Unit } -> Effect Unit -syncSpec (Runtime runtime) input = do - Ref.write input.activityUpdate runtime.activityUpdate +syncSpec (Runtime runtime) runInAff input = do + Ref.write (RunInAff runInAff) runtime.runInAff Ref.write input.spec runtime.spec Ref.write input.stateUpdate runtime.stateUpdate -activate :: forall props state action key. Ord key => Runtime props state action key -> Effect Unit +activate :: forall props state action m. Runtime props state action m -> Effect Unit activate runtime@(Runtime state) = do activeScope <- Ref.read state.scope case activeScope of @@ -202,16 +214,16 @@ activate runtime@(Runtime state) = do Nothing -> do generation <- fresh runtime active <- Ref.new true - roots <- Ref.new Map.empty + forks <- Ref.new Map.empty + handlers <- Ref.new Map.empty subscriptions <- Ref.new Map.empty - tasks <- Ref.new Map.empty - let scope = Scope { active, generation, roots, subscriptions, tasks } + let scope = Scope { active, forks, generation, handlers, subscriptions } Ref.write (Just scope) state.scope spec <- Ref.read state.spec startHandler runtime scope ActivationError spec.handlers.onActivate -deactivate :: forall props state action key. Ord key => Runtime props state action key -> Effect Unit -deactivate runtime@(Runtime state) = do +deactivate :: forall props state action m. Runtime props state action m -> Effect Unit +deactivate (Runtime state) = do activeScope <- Ref.read state.scope case activeScope of Nothing -> pure unit @@ -219,18 +231,18 @@ deactivate runtime@(Runtime state) = do Ref.write false current.active Ref.write Nothing state.scope - roots <- takeRef current.roots Map.empty - tasks <- takeRef current.tasks Map.empty + handlers <- takeRef current.handlers Map.empty + forks <- takeRef current.forks Map.empty subscriptions <- takeRef current.subscriptions Map.empty + let roots = Map.values handlers <> Map.values forks - publishActivity runtime emptyActivity + -- Fence every root before invoking foreign cleanup or requesting + -- cooperative Aff cancellation. + traverse_ fenceRoot roots cleanupResults <- traverse Exception.try (Map.values subscriptions) - traverse_ cancelRoot (Map.values roots) - traverse_ (traverse_ cancelRoot <<< Map.values <<< _.running) (Map.values tasks) + traverse_ requestCancel roots - -- A faulty external cleanup must not prevent the rest of the scope from - -- being cancelled. Report teardown failures only after every owned - -- resource has received its cleanup request. + -- A faulty external cleanup must not prevent any other cleanup request. spec <- Ref.read state.spec traverse_ ( case _ of @@ -240,9 +252,8 @@ deactivate runtime@(Runtime state) = do cleanupResults updateProps - :: forall props state action key - . Ord key - => Runtime props state action key + :: forall props state action m + . Runtime props state action m -> props -> Effect Unit updateProps runtime@(Runtime state) newProps = do @@ -258,9 +269,8 @@ updateProps runtime@(Runtime state) newProps = do activeScope dispatch - :: forall props state action key - . Ord key - => Runtime props state action key + :: forall props state action m + . Runtime props state action m -> action -> Effect Unit dispatch runtime@(Runtime state) action = do @@ -268,10 +278,9 @@ dispatch runtime@(Runtime state) action = do traverse_ (\scope -> dispatchToScope runtime scope action) activeScope dispatchToScope - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key + :: forall props state action m + . Runtime props state action m + -> Scope -> action -> Effect Unit dispatchToScope runtime@(Runtime state) scope action = do @@ -281,387 +290,188 @@ dispatchToScope runtime@(Runtime state) scope action = do startHandler runtime scope (ActionError action) (spec.handlers.onAction action) -- | Read the latest component props. -getProps :: forall props state action key. HaloM props state action key props -getProps = HaloM do - execution <- ask +getProps :: forall props state action m. HaloM props state action m props +getProps = HaloM $ ReaderT \execution -> do let Runtime runtime = execution.runtime liftEffect $ Ref.read runtime.props --- | Internal capability used by the abstract public Task API. -performTask - :: forall props state action key input - . Ord key - => Task (HaloM props state action key) key input - -> input - -> HaloM props state action key Unit -performTask task input = HaloM do - execution <- ask - liftEffect do - current <- isCurrent execution - when current do - configured <- registerStrategy execution.runtime (Task.key task) (Task.strategy task) - case configured of - StrategyAccepted -> - scheduleTask execution.runtime execution.scope (Task.key task) (Task.strategy task) - { computation: Task.run task input } - StrategyConflict previous -> do - let Runtime runtime = execution.runtime - spec <- Ref.read runtime.spec - spec.onError (TaskConfigurationError (Task.key task)) - ( Exception.error $ - "Task key was already defined as " <> Task.strategyName previous - <> " and cannot also be defined as " - <> Task.strategyName (Task.strategy task) - ) - --- | Internal capability used by the abstract public Task API. -cancelDefinition - :: forall props state action key input - . Ord key - => Task (HaloM props state action key) key input - -> HaloM props state action key Unit -cancelDefinition task = HaloM do - execution <- ask - liftEffect do - current <- isCurrent execution - when current $ - cancelKeyedTasks execution.runtime execution.scope (Task.key task) - --- | Register an emitter in the active component scope. Its cleanup runs on --- | manual unsubscription or scope deactivation. +-- | Start work owned by the current React activation. The fork may outlive its +-- | launching handler and is cancelled on explicit `kill` or deactivation. +fork + :: forall props state action m + . HaloM props state action m Unit + -> HaloM props state action m ForkId +fork child = HaloM $ ReaderT \execution -> do + fid <- liftEffect $ ForkId <$> fresh execution.runtime + current <- liftEffect $ isCurrent execution + when current do + prepared <- liftEffect $ prepare execution.runInAff execution.runtime execution.scope (ForkError fid) child do + let Scope scope = execution.scope + Ref.modify_ (Map.delete fid) scope.forks + liftEffect do + let Scope scope = execution.scope + Ref.modify_ (Map.insert fid prepared.root) scope.forks + prepared.start + pure fid + +-- | Cancel a component-owned fork. Halo fences the fork synchronously, then +-- | waits for its Aff cancellation and finalizers before returning. +kill + :: forall props state action m + . ForkId + -> HaloM props state action m Unit +kill fid = HaloM $ ReaderT \execution -> do + current <- liftEffect $ isCurrent execution + when current do + let Scope scope = execution.scope + root <- liftEffect $ Ref.modify' + ( \forks -> + { state: Map.delete fid forks + , value: Map.lookup fid forks + } + ) + scope.forks + traverse_ + ( \forkRoot -> do + liftEffect $ fenceRoot forkRoot + cancelRootAff forkRoot + ) + root + +-- | Register an emitter in the current activation scope. Its cleanup runs on +-- | manual unsubscription or deactivation. subscribe - :: forall props state action key - . Ord key - => Emitter action - -> HaloM props state action key SubscriptionId + :: forall props state action m + . Emitter action + -> HaloM props state action m SubscriptionId subscribe = subscribeWithId <<< const -- | Subscribe while providing the allocated identifier to the emitter. subscribeWithId - :: forall props state action key - . Ord key - => (SubscriptionId -> Emitter action) - -> HaloM props state action key SubscriptionId -subscribeWithId makeEmitter = HaloM do - execution <- ask - liftEffect do - sid <- SubscriptionId <$> fresh execution.runtime - current <- isCurrent execution - when current do + :: forall props state action m + . (SubscriptionId -> Emitter action) + -> HaloM props state action m SubscriptionId +subscribeWithId makeEmitter = HaloM $ ReaderT \execution -> do + sid <- liftEffect $ SubscriptionId <$> fresh execution.runtime + current <- liftEffect $ isCurrent execution + when current do + cleanup <- liftEffect $ Subscription.runEmitter (makeEmitter sid) + (dispatchToScope execution.runtime execution.scope) + liftEffect do let Scope scope = execution.scope - cleanup <- Subscription.runEmitter (makeEmitter sid) (dispatchToScope execution.runtime execution.scope) Ref.modify_ (Map.insert sid cleanup) scope.subscriptions - pure sid + pure sid -- | Remove a tracked subscription before running its cleanup. A throwing -- | cleanup therefore cannot be retried during deactivation. unsubscribe - :: forall props state action key + :: forall props state action m . SubscriptionId - -> HaloM props state action key Unit -unsubscribe sid = HaloM do - execution <- ask - liftEffect do - current <- isCurrent execution - when current do - let Scope scope = execution.scope - subscription <- Ref.modify' - ( \subscriptions -> - { state: Map.delete sid subscriptions - , value: Map.lookup sid subscriptions - } - ) - scope.subscriptions - traverse_ identity subscription - --- | Start a structured child of the current handler or task. The child is --- | cancelled when its parent finishes or is cancelled. -fork - :: forall props state action key - . HaloM props state action key Unit - -> HaloM props state action key ForkId -fork child = HaloM do - execution <- ask - liftEffect do - fid <- ForkId <$> fresh execution.runtime - current <- isCurrent execution - when current do - prepared <- prepare (Just execution.owner) execution.runtime execution.scope execution.context child \_ -> do - let Owner parent = execution.owner - Ref.modify_ (Map.delete fid) parent.children - let Owner parent = execution.owner - Ref.modify_ (Map.insert fid prepared.root) parent.children - prepared.start - pure fid - --- | Cancel a structured child before its parent finishes. -kill - :: forall props state action key - . ForkId - -> HaloM props state action key Unit -kill fid = HaloM do - execution <- ask - let Owner parent = execution.owner - child <- liftEffect $ Ref.modify' - ( \children -> - { state: Map.delete fid children - , value: Map.lookup fid children - } - ) - parent.children - traverse_ (liftAff <<< cancelRootAff) child + -> HaloM props state action m Unit +unsubscribe sid = HaloM $ ReaderT \execution -> do + current <- liftEffect $ isCurrent execution + when current do + let Scope scope = execution.scope + subscription <- liftEffect $ Ref.modify' + ( \subscriptions -> + { state: Map.delete sid subscriptions + , value: Map.lookup sid subscriptions + } + ) + scope.subscriptions + liftEffect $ traverse_ identity subscription startHandler - :: forall props state action key - . Runtime props state action key - -> Scope props state action key - -> ErrorContext props action key - -> HaloM props state action key Unit - -> Effect Unit -startHandler runtime scope@(Scope current) context computation = do - runId <- fresh runtime - prepared <- prepare Nothing runtime scope context computation \_ -> - Ref.modify_ (Map.delete runId) current.roots - Ref.modify_ (Map.insert runId prepared.root) current.roots - prepared.start - -registerStrategy - :: forall props state action key - . Ord key - => Runtime props state action key - -> key - -> Strategy - -> Effect StrategyRegistration -registerStrategy (Runtime runtime) key requested = Ref.modify' update runtime.strategies - where - update strategies = case Map.lookup key strategies of - Nothing -> - { state: Map.insert key requested strategies - , value: StrategyAccepted - } - Just existing | existing == requested -> - { state: strategies, value: StrategyAccepted } - Just existing -> - { state: strategies, value: StrategyConflict existing } - -scheduleTask - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key - -> key - -> Strategy - -> TaskRequest props state action key - -> Effect Unit -scheduleTask runtime scope@(Scope current) key strategy request = case strategy of - Concurrent -> startKeyed runtime scope key request - Restartable -> do - cancelKeyedTasks runtime scope key - startKeyed runtime scope key request - Drop -> do - tasks <- Ref.read current.tasks - let busy = maybe false (\slot -> not Map.isEmpty slot.running || not Array.null slot.queued) (Map.lookup key tasks) - unless busy $ startKeyed runtime scope key request - Enqueue -> enqueueOrStart runtime scope key request false - KeepLatest -> enqueueOrStart runtime scope key request true - -cancelKeyedTasks - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key - -> key - -> Effect Unit -cancelKeyedTasks runtime scope@(Scope current) key = do - tasks <- Ref.read current.tasks - let previous = maybe mempty (Map.values <<< _.running) (Map.lookup key tasks) - Ref.modify_ (Map.delete key) current.tasks - traverse_ cancelRoot previous - notifyActivity runtime scope - -startKeyed - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key - -> key - -> TaskRequest props state action key + :: forall props state action m + . Runtime props state action m + -> Scope + -> ErrorContext props action + -> HaloM props state action m Unit -> Effect Unit -startKeyed runtime scope@(Scope current) key request = do +startHandler runtime@(Runtime state) scope@(Scope current) context computation = do runId <- fresh runtime - prepared <- prepare Nothing runtime scope (TaskError key) request.computation \_ -> - completeKeyed runtime scope key runId - Ref.modify_ (Map.alter (Just <<< addRun runId prepared.root <<< maybe emptySlot identity) key) current.tasks - notifyActivity runtime scope + runInAff <- Ref.read state.runInAff + prepared <- prepare runInAff runtime scope context computation do + Ref.modify_ (Map.delete runId) current.handlers + Ref.modify_ (Map.insert runId prepared.root) current.handlers prepared.start -enqueueOrStart - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key - -> key - -> TaskRequest props state action key - -> Boolean - -> Effect Unit -enqueueOrStart runtime scope@(Scope current) key request keepOnlyLatest = do - tasks <- Ref.read current.tasks - case Map.lookup key tasks of - Just slot | not Map.isEmpty slot.running -> do - let queued = if keepOnlyLatest then [ request ] else Array.snoc slot.queued request - Ref.modify_ (Map.insert key (slot { queued = queued })) current.tasks - notifyActivity runtime scope - _ -> startKeyed runtime scope key request - -completeKeyed - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key - -> key - -> Int - -> Effect Unit -completeKeyed runtime scope@(Scope current) key runId = do - tasks <- Ref.read current.tasks - case Map.lookup key tasks of - Nothing -> pure unit - Just slot | not (Map.member runId slot.running) -> pure unit - Just slot -> do - let running = Map.delete runId slot.running - case Array.uncons slot.queued of - Just { head, tail } | Map.isEmpty running -> do - Ref.modify_ (Map.insert key { running, queued: tail }) current.tasks - notifyActivity runtime scope - startKeyed runtime scope key head - _ -> do - if Map.isEmpty running && Array.null slot.queued then - Ref.modify_ (Map.delete key) current.tasks - else - Ref.modify_ (Map.insert key (slot { running = running })) current.tasks - notifyActivity runtime scope - prepare - :: forall props state action key - . Maybe (Owner props state action key) - -> Runtime props state action key - -> Scope props state action key - -> ErrorContext props action key - -> HaloM props state action key Unit - -> (Owner props state action key -> Effect Unit) - -> Effect (Prepared props state action key) -prepare parent runtime scope context computation onComplete = do - owner <- createOwner parent + :: forall props state action m + . RunInAff m + -> Runtime props state action m + -> Scope + -> ErrorContext props action + -> HaloM props state action m Unit + -> Effect Unit + -> Effect Prepared +prepare runInAff runtime scope context computation onComplete = do + owner <- createOwner gate <- EffectAVar.empty - fiber <- Aff.launchAff $ do + fiber <- Aff.launchAff do void $ AVar.take gate Aff.finally - (closeOwner owner *> liftEffect (onComplete owner)) + ( liftEffect do + let Owner current = owner + Ref.write false current.alive + onComplete + ) do - outcome <- Aff.attempt $ runHaloM { context, owner, runtime, scope } computation + outcome <- Aff.attempt $ Aff.supervise $ + runHaloM { context, owner, runInAff, runtime, scope } computation case outcome of Left error -> do - current <- liftEffect $ isCurrent { context, owner, runtime, scope } + current <- liftEffect $ isCurrent { context, owner, runInAff, runtime, scope } when current do let Runtime state = runtime spec <- liftEffect $ Ref.read state.spec liftEffect $ spec.onError context error Right _ -> pure unit - let root = Root { fiber, owner } pure - { root + { root: Root { fiber, owner } , start: Aff.launchAff_ (AVar.put unit gate) } runHaloM - :: forall props state action key a - . Execution props state action key - -> HaloM props state action key a + :: forall props state action m a + . Execution props state action m + -> HaloM props state action m a -> Aff a -runHaloM execution (HaloM computation) = runReaderT computation execution +runHaloM execution (HaloM computation) = case computation of + ReaderT run -> run execution -createOwner - :: forall props state action key - . Maybe (Owner props state action key) - -> Effect (Owner props state action key) -createOwner parent = do +createOwner :: Effect Owner +createOwner = do alive <- Ref.new true - children <- Ref.new Map.empty - let - ancestors = case parent of - Just (Owner owner) -> owner.lineage - Nothing -> [] - pure $ Owner { alive, children, lineage: Array.cons alive ancestors } - -closeOwner :: forall props state action key. Owner props state action key -> Aff Unit -closeOwner (Owner owner) = do - children <- liftEffect do - Ref.write false owner.alive - takeRef owner.children Map.empty - traverse_ cancelRootAff (Map.values children) - -cancelRoot :: forall props state action key. Root props state action key -> Effect Unit -cancelRoot root@(Root current) = do - let Owner owner = current.owner - -- Fence commits synchronously. Fiber cancellation is asynchronous and cannot - -- stop external effects that have already happened. - Ref.write false owner.alive - Aff.launchAff_ (cancelRootAff root) + pure $ Owner { alive } -cancelRootAff :: forall props state action key. Root props state action key -> Aff Unit -cancelRootAff (Root root) = do +fenceRoot :: Root -> Effect Unit +fenceRoot (Root root) = do let Owner owner = root.owner - liftEffect $ Ref.write false owner.alive - Aff.killFiber (Aff.error "Halo scope cancelled") root.fiber + Ref.write false owner.alive + +requestCancel :: Root -> Effect Unit +requestCancel root = Aff.launchAff_ (cancelRootAff root) + +cancelRootAff :: Root -> Aff Unit +cancelRootAff root@(Root current) = do + liftEffect $ fenceRoot root + Aff.killFiber (Aff.error "Halo scope cancelled") current.fiber isCurrent - :: forall props state action key - . Execution props state action key + :: forall props state action m + . Execution props state action m -> Effect Boolean isCurrent execution = do let Owner owner = execution.owner - let Scope scope = execution.scope - let Runtime runtime = execution.runtime - ownerAlive <- and <$> traverse Ref.read owner.lineage - scopeActive <- Ref.read scope.active - activeScope <- Ref.read runtime.scope - pure $ ownerAlive && scopeActive && case activeScope of - Just (Scope active) -> active.generation == scope.generation - Nothing -> false - -notifyActivity - :: forall props state action key - . Ord key - => Runtime props state action key - -> Scope props state action key - -> Effect Unit -notifyActivity runtime@(Runtime state) scope@(Scope current) = do - active <- isScopeCurrent runtime scope - when active do - tasks <- Ref.read current.tasks - let - counts slot = - { running: Map.size slot.running - , queued: Array.length slot.queued - } - byKey = map counts tasks - total = foldl addCounts { running: 0, queued: 0 } (Map.values byKey) - activity = Activity { total, byKey } - update <- Ref.read state.activityUpdate - update activity - -publishActivity - :: forall props state action key - . Runtime props state action key - -> Activity key - -> Effect Unit -publishActivity (Runtime runtime) activity = do - update <- Ref.read runtime.activityUpdate - update activity + ownerAlive <- Ref.read owner.alive + scopeCurrent <- isScopeCurrent execution.runtime execution.scope + pure (ownerAlive && scopeCurrent) isScopeCurrent - :: forall props state action key - . Runtime props state action key - -> Scope props state action key + :: forall props state action m + . Runtime props state action m + -> Scope -> Effect Boolean isScopeCurrent (Runtime runtime) (Scope scope) = do scopeActive <- Ref.read scope.active @@ -670,24 +480,7 @@ isScopeCurrent (Runtime runtime) (Scope scope) = do Just (Scope active) -> active.generation == scope.generation Nothing -> false -addCounts :: { running :: Int, queued :: Int } -> { running :: Int, queued :: Int } -> { running :: Int, queued :: Int } -addCounts left right = - { running: left.running + right.running - , queued: left.queued + right.queued - } - -emptySlot :: forall props state action key. TaskSlot props state action key -emptySlot = { queued: [], running: Map.empty } - -addRun - :: forall props state action key - . Int - -> Root props state action key - -> TaskSlot props state action key - -> TaskSlot props state action key -addRun runId root slot = slot { running = Map.insert runId root slot.running } - -fresh :: forall props state action key. Runtime props state action key -> Effect Int +fresh :: forall props state action m. Runtime props state action m -> Effect Int fresh (Runtime runtime) = Ref.modify' (\value -> { state: value + 1, value }) runtime.fresh takeRef :: forall a. Ref a -> a -> Effect a diff --git a/src/React/Halo/Internal/Task.purs b/src/React/Halo/Internal/Task.purs deleted file mode 100644 index 02058c1..0000000 --- a/src/React/Halo/Internal/Task.purs +++ /dev/null @@ -1,41 +0,0 @@ -module React.Halo.Internal.Task - ( Strategy(..) - , Task(..) - , key - , run - , strategy - , strategyName - ) where - -import Prelude - --- | Runtime-only scheduling modes. Public code chooses one by constructing a --- | first-class task definition. -data Strategy - = Concurrent - | Restartable - | Drop - | Enqueue - | KeepLatest - -derive instance eqStrategy :: Eq Strategy - --- | Internal representation parameterized by its computation monad. -data Task m key input = Task key Strategy (input -> m Unit) - -key :: forall m key input. Task m key input -> key -key (Task taskKey _ _) = taskKey - -strategy :: forall m key input. Task m key input -> Strategy -strategy (Task _ taskStrategy _) = taskStrategy - -run :: forall m key input. Task m key input -> input -> m Unit -run (Task _ _ implementation) = implementation - -strategyName :: Strategy -> String -strategyName = case _ of - Concurrent -> "concurrent" - Restartable -> "restartable" - Drop -> "drop" - Enqueue -> "enqueue" - KeepLatest -> "keepLatest" diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index bd38305..a87129c 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -1,67 +1,28 @@ module React.Halo.Internal.Types - ( Activity(..) - , ErrorContext(..) + ( ErrorContext(..) , ForkId(..) , SubscriptionId(..) - , TaskCounts - , activityAtKey - , totalActivity - , emptyActivity ) where import Prelude -import Data.Map (Map) -import Data.Map as Map -import Data.Maybe (Maybe(..)) - --- | Identifies the operation whose unexpected failure reached `onError`. --- | --- | `PropsChangeError` carries the previous props. Task failures and task --- | configuration conflicts carry the affected task key. -data ErrorContext props action key +-- | Identifies the component-owned computation whose unexpected failure reached +-- | `onError`. +data ErrorContext props action = ActivationError | PropsChangeError props | ActionError action - | TaskError key - | TaskConfigurationError key + | ForkError ForkId | DeactivationError --- | Counts of explicit scheduled tasks. Handler and structured-child execution --- | is intentionally excluded. -type TaskCounts = - { running :: Int - , queued :: Int - } - --- | A renderable snapshot of explicit task activity. Every task is keyed, so --- | the total is the sum of the per-key counts. -newtype Activity key = Activity - { total :: TaskCounts - , byKey :: Map key TaskCounts - } - -derive newtype instance eqActivity :: Eq key => Eq (Activity key) - -derive newtype instance showActivity :: Show key => Show (Activity key) +-- | Identifies a component-owned fiber created with `fork`. +newtype ForkId = ForkId Int --- | An activity snapshot with no running or queued tasks. -emptyActivity :: forall key. Activity key -emptyActivity = Activity - { total: { running: 0, queued: 0 } - , byKey: Map.empty - } +derive newtype instance eqForkId :: Eq ForkId --- | Read total running and queued explicit task counts. -totalActivity :: forall key. Activity key -> TaskCounts -totalActivity (Activity activity) = activity.total +derive newtype instance ordForkId :: Ord ForkId --- | Internal keyed lookup used by the abstract Task API. -activityAtKey :: forall key. Ord key => key -> Activity key -> TaskCounts -activityAtKey key (Activity activity) = - case Map.lookup key activity.byKey of - Just counts -> counts - Nothing -> { running: 0, queued: 0 } +derive newtype instance showForkId :: Show ForkId -- | Identifies a component-scoped emitter subscription. newtype SubscriptionId = SubscriptionId Int @@ -71,12 +32,3 @@ derive newtype instance eqSubscriptionId :: Eq SubscriptionId derive newtype instance ordSubscriptionId :: Ord SubscriptionId derive newtype instance showSubscriptionId :: Show SubscriptionId - --- | Identifies a structured child created with `fork`. -newtype ForkId = ForkId Int - -derive newtype instance eqForkId :: Eq ForkId - -derive newtype instance ordForkId :: Ord ForkId - -derive newtype instance showForkId :: Show ForkId diff --git a/src/React/Halo/Task.purs b/src/React/Halo/Task.purs deleted file mode 100644 index 1f8bc76..0000000 --- a/src/React/Halo/Task.purs +++ /dev/null @@ -1,106 +0,0 @@ -module React.Halo.Task - ( Task - , activity - , cancel - , concurrent - , drop - , enqueue - , keepLatest - , perform - , perform_ - , restartable - ) where - -import Prelude - -import React.Halo.Internal.Runtime (HaloM) -import React.Halo.Internal.Runtime as Runtime -import React.Halo.Internal.Task as Internal -import React.Halo.Internal.Types (Activity, TaskCounts, activityAtKey) - --- | A reusable task definition. A task binds a user-defined key, one scheduling --- | strategy, and an input-driven Halo computation. Its constructor is hidden; --- | create tasks with `concurrent`, `restartable`, `drop`, `enqueue`, or --- | `keepLatest`. -newtype Task props state action key input = Task - (Internal.Task (HaloM props state action key) key input) - --- | Define a task whose performances for this key all run concurrently. -concurrent - :: forall props state action key input - . key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input -concurrent key implementation = Task (Internal.Task key Internal.Concurrent implementation) - --- | Define a task whose newest performance cancels and replaces running and --- | queued work for this key. -restartable - :: forall props state action key input - . key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input -restartable key implementation = Task (Internal.Task key Internal.Restartable implementation) - --- | Define a task that ignores a performance while this key is busy. -drop - :: forall props state action key input - . key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input -drop key implementation = Task (Internal.Task key Internal.Drop implementation) - --- | Define a task that runs every performance for this key FIFO, one at a time. -enqueue - :: forall props state action key input - . key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input -enqueue key implementation = Task (Internal.Task key Internal.Enqueue implementation) - --- | Define a task that lets current work finish while retaining only the newest --- | queued performance for this key. -keepLatest - :: forall props state action key input - . key - -> (input -> HaloM props state action key Unit) - -> Task props state action key input -keepLatest key implementation = Task (Internal.Task key Internal.KeepLatest implementation) - --- | Submit one task input and return immediately. The resulting work belongs to --- | the active component scope, not to the handler or task that submitted it. -perform - :: forall props state action key input - . Ord key - => Task props state action key input - -> input - -> HaloM props state action key Unit -perform (Task task) = Runtime.performTask task - --- | Submit a task whose input is `Unit`. -perform_ - :: forall props state action key - . Ord key - => Task props state action key Unit - -> HaloM props state action key Unit -perform_ task = perform task unit - --- | Fence and cancel all running work and discard all queued work for the task's --- | key. Definitions that intentionally share the key share this cancellation --- | boundary. -cancel - :: forall props state action key input - . Ord key - => Task props state action key input - -> HaloM props state action key Unit -cancel (Task task) = Runtime.cancelDefinition task - --- | Read running and queued activity for the task's key. Definitions that share --- | the key report the same counts. -activity - :: forall props state action key input - . Ord key - => Task props state action key input - -> Activity key - -> TaskCounts -activity (Task task) = activityAtKey (Internal.key task) diff --git a/test/Main.purs b/test/Main.purs index 59b3b6f..2dc209c 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -4,13 +4,13 @@ import Prelude import Effect (Effect) import Test.Halo.ScopeHandlerSpec as ScopeHandlerSpec -import Test.Halo.SchedulerSpec as SchedulerSpec +import Test.Halo.RuntimeSpec as RuntimeSpec import Test.Halo.SubscriptionErrorSpec as SubscriptionErrorSpec import Test.Spec.Reporter (consoleReporter) import Test.Spec.Runner.Node (runSpecAndExitProcess) main :: Effect Unit main = runSpecAndExitProcess [ consoleReporter ] do - SchedulerSpec.spec + RuntimeSpec.spec ScopeHandlerSpec.spec SubscriptionErrorSpec.spec diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index 8e9024b..7e8748d 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -2,134 +2,134 @@ module Test.Halo.DocExamples where import Prelude -import Control.Monad.State (modify_) +import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.State (gets, modify_) +import Control.Monad.Trans.Class (lift) +import Control.Parallel (parallel, sequential) import Data.Either (Either(..)) +import Data.Foldable (traverse_) import Data.Maybe (Maybe(..)) +import Data.Tuple (Tuple(..)) import Effect.Aff (Aff, attempt) -import Effect.Aff.Class (liftAff) +import Effect.Aff.Class (class MonadAff, liftAff) +import Effect.Class (class MonadEffect) import Effect.Class.Console as Console import Effect.Exception (message) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) -import React.Basic.Hooks (Component) +import React.Basic.Hooks (Component, Hook) import React.Halo as Halo -type Props = { loadGreeting :: Aff String } +type Env = { loadGreeting :: Aff String } + +newtype AppM a = AppM (ReaderT Env Aff a) + +derive newtype instance functorAppM :: Functor AppM +derive newtype instance applyAppM :: Apply AppM +derive newtype instance applicativeAppM :: Applicative AppM +derive newtype instance bindAppM :: Bind AppM +derive newtype instance monadAppM :: Monad AppM +derive newtype instance monadEffectAppM :: MonadEffect AppM +derive newtype instance monadAffAppM :: MonadAff AppM + +runAppM :: Env -> AppM ~> Aff +runAppM env (AppM computation) = runReaderT computation env + +loadGreeting :: AppM (Either String String) +loadGreeting = AppM do + env <- ask + outcome <- liftAff $ attempt env.loadGreeting + pure case outcome of + Left error -> Left (message error) + Right greeting -> Right greeting + +type Props = { title :: String } type State = - { loading :: Boolean + { fiber :: Maybe Halo.ForkId + , loading :: Boolean , result :: Maybe (Either String String) } -data Action = Load - -data TaskKey = GreetingRequest - -derive instance eqTaskKey :: Eq TaskKey -derive instance ordTaskKey :: Ord TaskKey - -loadGreetingTask :: Halo.Task Props State Action TaskKey Unit -loadGreetingTask = Halo.restartable GreetingRequest \_ -> do - modify_ _ { loading = true, result = Nothing } - { loadGreeting } <- Halo.getProps - outcome <- liftAff $ attempt loadGreeting - modify_ _ - { loading = false - , result = Just $ case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting - } - -loadButton :: Component Props -loadButton = Halo.component "LoadButton" - { initialState: \_ -> { loading: false, result: Nothing } - , handlers: Halo.defaultHandlers - { onAction = \Load -> Halo.perform_ loadGreetingTask } +data Action + = Load + | Cancel + +type UI a = Halo.HaloM Props State Action AppM a + +handlers :: Halo.Handlers Props State Action AppM +handlers = Halo.defaultHandlers + { onAction = case _ of + Load -> do + previous <- gets _.fiber + traverse_ Halo.kill previous + fiber <- Halo.fork do + modify_ _ { loading = true, result = Nothing } + result <- lift loadGreeting + modify_ _ { loading = false, result = Just result } + modify_ _ { fiber = Just fiber } + Cancel -> do + previous <- gets _.fiber + traverse_ Halo.kill previous + modify_ _ { fiber = Nothing, loading = false } + } + +loadButton :: Env -> Component Props +loadButton env = Halo.component "LoadButton" (runAppM env) + { initialState: \_ -> { fiber: Nothing, loading: false, result: Nothing } + , handlers , onError: \context error -> Console.error $ "Unexpected Halo failure in " <> showContext context <> ": " <> message error - , render: \{ state, dispatch, activity } -> - let - counts = Halo.activity loadGreetingTask activity - in - R.div_ - [ R.button - { onClick: capture_ (dispatch Load) - , children: [ R.text if counts.running > 0 then "Restart load" else "Load" ] - } - , R.text $ case state.result of - Nothing -> if state.loading then "Loading…" else "Not loaded" - Just (Left error) -> error - Just (Right greeting) -> greeting - ] + , render: \{ props, state, dispatch } -> + R.div_ + [ R.text props.title + , R.button + { onClick: capture_ (dispatch Load) + , children: [ R.text if state.loading then "Restart" else "Load" ] + } + , R.button + { onClick: capture_ (dispatch Cancel) + , children: [ R.text "Cancel" ] + } + , R.text $ case state.result of + Nothing -> if state.loading then "Loading…" else "Not loaded" + Just (Left error) -> error + Just (Right greeting) -> greeting + ] + } + +useExample + :: Env + -> Props + -> Hook (Halo.UseHalo Props State Action AppM) (Halo.HaloResult State Action) +useExample env props = Halo.useHalo (runAppM env) + { props + , initialState: { fiber: Nothing, loading: false, result: Nothing } + , handlers + , onError: \_ _ -> pure unit } -showContext :: Halo.ErrorContext Props Action TaskKey -> String +parallelExample :: UI Unit +parallelExample = do + Tuple a b <- sequential ado + a <- parallel $ lift (pure 1 :: AppM Int) + b <- parallel $ lift (pure 2 :: AppM Int) + in Tuple a b + modify_ _ { loading = a + b < 0 } + +showContext :: Halo.ErrorContext Props Action -> String showContext = case _ of Halo.ActivationError -> "activation" - Halo.DeactivationError -> "deactivation" Halo.PropsChangeError _ -> "props change" Halo.ActionError Load -> "Load action" - Halo.TaskError GreetingRequest -> "greeting task" - Halo.TaskConfigurationError GreetingRequest -> "greeting task definition" - -data WorkflowAction - = SearchChanged String - | SaveClicked - | Autosave String - | UploadChunk Int Int - | RecordMetric String - -data WorkflowTask - = SearchRequest - | SaveRequest - | AutosaveRequest - | Upload Int - | Metrics - -derive instance eqWorkflowTask :: Eq WorkflowTask -derive instance ordWorkflowTask :: Ord WorkflowTask - -searchTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask String -searchTask = Halo.restartable SearchRequest \_ -> pure unit - -saveTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask Unit -saveTask = Halo.drop SaveRequest \_ -> pure unit - -autosaveTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask String -autosaveTask = Halo.keepLatest AutosaveRequest \_ -> pure unit - -uploadTask :: Int -> Halo.Task Unit Unit WorkflowAction WorkflowTask Int -uploadTask fileId = Halo.enqueue (Upload fileId) \_ -> pure unit - -metricTask :: Halo.Task Unit Unit WorkflowAction WorkflowTask String -metricTask = Halo.concurrent Metrics \_ -> pure unit - -handleWorkflow - :: WorkflowAction - -> Halo.HaloM Unit Unit WorkflowAction WorkflowTask Unit -handleWorkflow = case _ of - SearchChanged query -> Halo.perform searchTask query - SaveClicked -> Halo.perform_ saveTask - Autosave draft -> Halo.perform autosaveTask draft - UploadChunk fileId chunk -> Halo.perform (uploadTask fileId) chunk - RecordMetric name -> Halo.perform metricTask name + Halo.ActionError Cancel -> "Cancel action" + Halo.ForkError _ -> "fork" + Halo.DeactivationError -> "deactivation" data SimpleAction = InitializeData simpleEmitter :: Halo.Emitter SimpleAction simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) -simpleHandlers :: Halo.Handlers Unit Unit SimpleAction Unit -simpleHandlers = Halo.defaultHandlers - { onActivate = pure unit - , onAction = \InitializeData -> pure unit - } - -simpleSubscription :: Halo.HaloM Unit Unit SimpleAction Unit Unit +simpleSubscription :: Halo.HaloM Unit Unit SimpleAction AppM Unit simpleSubscription = void $ Halo.subscribeWithId \_ -> simpleEmitter - -totalWorkflowActivity :: Halo.Activity WorkflowTask -> Halo.TaskCounts -totalWorkflowActivity = Halo.totalActivity - -readSimpleState :: Halo.HaloResult Unit SimpleAction Unit -> Unit -readSimpleState = _.state diff --git a/test/Test/Halo/Helpers.purs b/test/Test/Halo/Helpers.purs index 3627113..7466da3 100644 --- a/test/Test/Halo/Helpers.purs +++ b/test/Test/Halo/Helpers.purs @@ -1,168 +1,44 @@ module Test.Halo.Helpers - ( Action(..) - , Gate - , Harness - , Key(..) - , UnitTask - , WorkInput - , WorkTask + ( Gate , await - , awaitCounts - , handlers , makeGate - , makeHarness , release - , runGate , shouldNotHaveStarted - , withHarness - , work + , waitForGate ) where import Prelude import Control.Alt ((<|>)) -import Control.Monad.State (modify_) import Control.Parallel (parallel, sequential) -import Data.Array as Array import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Aff (Aff, Milliseconds(..)) import Effect.Aff as Aff import Effect.Aff.AVar as AVar -import Effect.Aff.Class (liftAff) import Effect.AVar (AVar) import Effect.AVar as EffectAVar -import Effect.Class (liftEffect) -import Effect.Exception (message) -import Effect.Ref (Ref) -import Effect.Ref as Ref -import React.Halo as Halo -import React.Halo.Handlers (Handlers, defaultHandlers) -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, fork) -import React.Halo.Internal.Types (Activity, ErrorContext(..), TaskCounts, emptyActivity, totalActivity) -import Test.Spec.Assertions (fail, shouldEqual) - -data Key = Search | Save - -derive instance eqKey :: Eq Key -derive instance ordKey :: Ord Key - -instance showKey :: Show Key where - show Search = "Search" - show Save = "Save" +import Test.Spec.Assertions (shouldEqual) type Gate = - { launched :: AVar Unit - , release :: AVar Unit + { release :: AVar Unit , settled :: AVar Unit , started :: AVar Unit } -type WorkInput = - { gate :: Gate - , value :: Int - } - -type WorkTask = Halo.Task Unit (Array Int) Action Key WorkInput - -type UnitTask = Halo.Task Unit (Array Int) Action Key Unit - -data Action - = Perform WorkTask WorkInput - | PerformWithWitness WorkTask WorkInput Gate - | PerformUnit UnitTask Gate - | Cancel WorkTask (AVar Unit) - | Direct Int Gate - | Boom Gate - -type Harness = - { activity :: Ref (Activity Key) - , activityChanged :: AVar Unit - , errors :: Ref (Array String) - , errorRaised :: AVar Unit - , runtime :: Runtime Unit (Array Int) Action Key - , state :: Ref (Array Int) - } - makeGate :: Effect Gate makeGate = do - launched <- EffectAVar.empty started <- EffectAVar.empty releaseGate <- EffectAVar.empty settled <- EffectAVar.empty - pure { launched, started, release: releaseGate, settled } - -runGate - :: forall props action key - . Int - -> Gate - -> HaloM props (Array Int) action key Unit -runGate value gate = do - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release - modify_ (flip Array.snoc value) - -work :: WorkInput -> HaloM Unit (Array Int) Action Key Unit -work input = runGate input.value input.gate - -handlers :: Handlers Unit (Array Int) Action Key -handlers = defaultHandlers - { onAction = case _ of - Perform task input -> do - Halo.perform task input - liftAff $ void $ AVar.tryPut unit input.gate.launched - PerformWithWitness task input witness -> do - Halo.perform task input - void $ fork (runGate 999 witness) - liftAff $ void $ AVar.take witness.started - liftAff $ void $ AVar.tryPut unit input.gate.launched - PerformUnit task gate -> do - Halo.perform_ task - liftAff $ void $ AVar.tryPut unit gate.launched - Cancel task completed -> do - Halo.cancel task - liftAff $ void $ AVar.tryPut unit completed - Direct value gate -> do - liftAff $ void $ AVar.tryPut unit gate.launched - runGate value gate - Boom gate -> do - liftAff $ void $ AVar.tryPut unit gate.launched - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - (Aff.throwError (Aff.error "boom")) - } - -makeHarness :: Aff Harness -makeHarness = liftEffect do - activity <- Ref.new emptyActivity - activityChanged <- EffectAVar.empty - errors <- Ref.new [] - errorRaised <- EffectAVar.empty - state <- Ref.new [] - runtime <- createRuntime - { activityUpdate: \next -> do - Ref.write next activity - void $ EffectAVar.tryPut unit activityChanged - , initialProps: unit - , initialState: [] - , spec: - { handlers - , onError: \context error -> do - Ref.modify_ (\current -> Array.snoc current (contextName context <> ": " <> message error)) errors - void $ EffectAVar.tryPut unit errorRaised - } - , stateUpdate: flip Ref.write state - } - activate runtime - pure { activity, activityChanged, errors, errorRaised, runtime, state } + pure { started, release: releaseGate, settled } -withHarness :: (Harness -> Aff Unit) -> Aff Unit -withHarness test = do - harness <- makeHarness - Aff.finally (liftEffect $ deactivate harness.runtime) (test harness) +waitForGate :: Gate -> Aff Unit +waitForGate gate = Aff.finally + (void $ AVar.tryPut unit gate.settled) + do + AVar.put unit gate.started + void $ AVar.take gate.release await :: forall a. String -> AVar a -> Aff a await label value = @@ -177,24 +53,3 @@ shouldNotHaveStarted :: Gate -> Aff Unit shouldNotHaveStarted gate = do started <- AVar.tryTake gate.started started `shouldEqual` Nothing - -awaitCounts :: Harness -> TaskCounts -> Aff Unit -awaitCounts harness expected = go 20 - where - go remaining = do - actual <- totalActivity <$> liftEffect (Ref.read harness.activity) - if actual == expected then pure unit - else if remaining <= 0 then - fail $ "Expected activity " <> show expected <> " but got " <> show actual - else do - void $ await "activity update" harness.activityChanged - go (remaining - 1) - -contextName :: ErrorContext Unit Action Key -> String -contextName = case _ of - ActivationError -> "activation" - DeactivationError -> "deactivation" - PropsChangeError _ -> "props" - ActionError _ -> "action" - TaskError key -> "task " <> show key - TaskConfigurationError key -> "task configuration " <> show key diff --git a/test/Test/Halo/RuntimeSpec.purs b/test/Test/Halo/RuntimeSpec.purs new file mode 100644 index 0000000..e0906f0 --- /dev/null +++ b/test/Test/Halo/RuntimeSpec.purs @@ -0,0 +1,125 @@ +module Test.Halo.RuntimeSpec (spec) where + +import Prelude + +import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.State (modify_) +import Control.Monad.Trans.Class (lift) +import Control.Parallel (parallel, sequential) +import Data.Tuple (Tuple(..)) +import Effect.Aff (Aff) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (class MonadAff, liftAff) +import Effect.AVar (AVar) +import Effect.AVar as EffectAVar +import Effect.Class (class MonadEffect, liftEffect) +import Effect.Ref as Ref +import React.Halo.Handlers (Handlers, defaultHandlers) +import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, syncSpec) +import Test.Halo.Helpers (Gate, await, makeGate, release, waitForGate) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +newtype AppM a = AppM (ReaderT Int Aff a) + +derive newtype instance functorAppM :: Functor AppM +derive newtype instance applyAppM :: Apply AppM +derive newtype instance applicativeAppM :: Applicative AppM +derive newtype instance bindAppM :: Bind AppM +derive newtype instance monadAppM :: Monad AppM +derive newtype instance monadEffectAppM :: MonadEffect AppM +derive newtype instance monadAffAppM :: MonadAff AppM + +runAppM :: Int -> AppM ~> Aff +runAppM environment (AppM computation) = runReaderT computation environment + +readEnvironment :: AppM Int +readEnvironment = AppM ask + +data Action + = ReadEnvironment Gate (AVar Int) + | RunParallel Gate Gate (AVar Unit) + +type State = Int + +handlers :: Handlers Unit State Action AppM +handlers = defaultHandlers + { onAction = case _ of + ReadEnvironment gate completed -> do + liftAff $ waitForGate gate + environment <- lift readEnvironment + liftAff $ void $ AVar.tryPut environment completed + RunParallel left right completed -> do + Tuple a b <- sequential ado + a <- parallel do + liftAff $ waitForGate left + pure 1 + b <- parallel do + liftAff $ waitForGate right + pure 2 + in Tuple a b + modify_ (\state -> state + a + b) + liftAff $ void $ AVar.tryPut unit completed + } + +makeRuntime + :: Int + -> Aff + { runtime :: Runtime Unit State Action AppM + , state :: Ref.Ref State + } +makeRuntime environment = liftEffect do + state <- Ref.new 0 + runtime <- createRuntime (runAppM environment) + { initialProps: unit + , initialState: 0 + , spec: { handlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write state + } + activate runtime + pure { runtime, state } + +spec :: Spec Unit +spec = describe "application monad and parallelism" do + it "lifts AppM and snapshots the interpreter for each root" do + { runtime, state } <- makeRuntime 1 + Aff.finally (liftEffect $ deactivate runtime) do + firstGate <- liftEffect makeGate + firstResult <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (ReadEnvironment firstGate firstResult) + void $ await "first root start" firstGate.started + + liftEffect $ syncSpec runtime (runAppM 2) + { spec: { handlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write state + } + + secondGate <- liftEffect makeGate + secondResult <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (ReadEnvironment secondGate secondResult) + void $ await "second root start" secondGate.started + + release firstGate + release secondGate + first <- await "first environment" firstResult + second <- await "second environment" secondResult + first `shouldEqual` 1 + second `shouldEqual` 2 + + it "runs Halo branches concurrently with the direct Parallel instance" do + { runtime, state } <- makeRuntime 1 + Aff.finally (liftEffect $ deactivate runtime) do + left <- liftEffect makeGate + right <- liftEffect makeGate + completed <- liftEffect EffectAVar.empty + + liftEffect $ dispatch runtime (RunParallel left right completed) + void $ await "left parallel branch" left.started + void $ await "right parallel branch" right.started + + release left + release right + void $ await "parallel handler completion" completed + value <- liftEffect $ Ref.read state + value `shouldEqual` 3 diff --git a/test/Test/Halo/SchedulerSpec.purs b/test/Test/Halo/SchedulerSpec.purs deleted file mode 100644 index 6485a04..0000000 --- a/test/Test/Halo/SchedulerSpec.purs +++ /dev/null @@ -1,229 +0,0 @@ -module Test.Halo.SchedulerSpec (spec) where - -import Prelude - -import Data.Array as Array -import Effect.AVar as EffectAVar -import Effect.Class (liftEffect) -import Effect.Ref as Ref -import React.Halo as Halo -import React.Halo.Internal.Runtime (activate, deactivate, dispatch) -import Test.Halo.Helpers (Action(..), Key(..), await, awaitCounts, makeGate, release, runGate, shouldNotHaveStarted, withHarness, work) -import Test.Spec (Spec, describe, it) -import Test.Spec.Assertions (shouldEqual) - -spec :: Spec Unit -spec = describe "first-class task scheduling" do - it "handles an action immediately without counting it as task activity" $ withHarness \harness -> do - gate <- liftEffect makeGate - liftEffect $ dispatch harness.runtime (Direct 1 gate) - void $ await "direct action start" gate.started - - awaitCounts harness { running: 0, queued: 0 } - release gate - void $ await "direct action completion" gate.settled - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 1 ] - - it "keeps performed work alive after its launching handler completes" $ withHarness \harness -> do - gate <- liftEffect makeGate - witness <- liftEffect makeGate - let task = Halo.concurrent Search work - liftEffect $ dispatch harness.runtime (PerformWithWitness task { value: 1, gate } witness) - - void $ await "task submission" gate.launched - void $ await "launching handler completion" witness.settled - void $ await "performed task start" gate.started - awaitCounts harness { running: 1, queued: 0 } - - release gate - void $ await "performed task completion" gate.settled - awaitCounts harness { running: 0, queued: 0 } - - it "concurrent preserves inputs and runs same-key performances together" $ withHarness \harness -> do - first <- liftEffect makeGate - second <- liftEffect makeGate - let - task = Halo.concurrent Search work - idleTask = Halo.concurrent Save work - - liftEffect do - dispatch harness.runtime (Perform task { value: 1, gate: first }) - dispatch harness.runtime (Perform task { value: 2, gate: second }) - - void $ await "first concurrent task start" first.started - void $ await "second concurrent task start" second.started - awaitCounts harness { running: 2, queued: 0 } - activity <- liftEffect $ Ref.read harness.activity - Halo.activity task activity `shouldEqual` { running: 2, queued: 0 } - Halo.activity idleTask activity `shouldEqual` { running: 0, queued: 0 } - - release second - release first - void $ await "first concurrent task completion" first.settled - void $ await "second concurrent task completion" second.settled - state <- liftEffect $ Ref.read harness.state - Array.sort state `shouldEqual` [ 1, 2 ] - - it "perform_ submits a unit-input task" $ withHarness \harness -> do - gate <- liftEffect makeGate - let task = Halo.concurrent Save \_ -> runGate 7 gate - liftEffect $ dispatch harness.runtime (PerformUnit task gate) - - void $ await "unit task start" gate.started - release gate - void $ await "unit task completion" gate.settled - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 7 ] - - it "restartable cancels and commit-fences previous work" $ withHarness \harness -> do - stale <- liftEffect makeGate - current <- liftEffect makeGate - let task = Halo.restartable Search work - - liftEffect $ dispatch harness.runtime (Perform task { value: 1, gate: stale }) - void $ await "stale restartable task start" stale.started - liftEffect $ dispatch harness.runtime (Perform task { value: 2, gate: current }) - - void $ await "stale restartable task cancellation" stale.settled - void $ await "replacement restartable task start" current.started - release current - void $ await "replacement restartable task completion" current.settled - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 2 ] - - it "drop ignores new input while its key is busy" $ withHarness \harness -> do - running <- liftEffect makeGate - dropped <- liftEffect makeGate - let task = Halo.drop Save work - - liftEffect $ dispatch harness.runtime (Perform task { value: 1, gate: running }) - void $ await "drop task start" running.started - liftEffect $ dispatch harness.runtime (Perform task { value: 2, gate: dropped }) - void $ await "dropped submission" dropped.launched - awaitCounts harness { running: 1, queued: 0 } - shouldNotHaveStarted dropped - - release running - void $ await "drop task completion" running.settled - shouldNotHaveStarted dropped - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 1 ] - - it "enqueue preserves every queued payload FIFO" $ withHarness \harness -> do - first <- liftEffect makeGate - second <- liftEffect makeGate - third <- liftEffect makeGate - let task = Halo.enqueue Save work - - liftEffect do - dispatch harness.runtime (Perform task { value: 1, gate: first }) - dispatch harness.runtime (Perform task { value: 2, gate: second }) - dispatch harness.runtime (Perform task { value: 3, gate: third }) - - void $ await "first enqueue task start" first.started - awaitCounts harness { running: 1, queued: 2 } - shouldNotHaveStarted second - shouldNotHaveStarted third - - release first - void $ await "second enqueue task start" second.started - release second - void $ await "third enqueue task start" third.started - release third - void $ await "third enqueue task completion" third.settled - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 1, 2, 3 ] - - it "keepLatest retains only the newest queued payload" $ withHarness \harness -> do - first <- liftEffect makeGate - discarded <- liftEffect makeGate - latest <- liftEffect makeGate - let task = Halo.keepLatest Search work - - liftEffect do - dispatch harness.runtime (Perform task { value: 1, gate: first }) - dispatch harness.runtime (Perform task { value: 2, gate: discarded }) - dispatch harness.runtime (Perform task { value: 3, gate: latest }) - - void $ await "current keepLatest task start" first.started - awaitCounts harness { running: 1, queued: 1 } - release first - void $ await "latest keepLatest task start" latest.started - shouldNotHaveStarted discarded - release latest - void $ await "latest keepLatest task completion" latest.settled - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 1, 3 ] - - it "cancel uses task identity to cancel running and queued work" $ withHarness \harness -> do - running <- liftEffect makeGate - queued <- liftEffect makeGate - cancelled <- liftEffect EffectAVar.empty - let task = Halo.enqueue Search work - - liftEffect do - dispatch harness.runtime (Perform task { value: 1, gate: running }) - dispatch harness.runtime (Perform task { value: 2, gate: queued }) - void $ await "task before cancellation" running.started - awaitCounts harness { running: 1, queued: 1 } - - liftEffect $ dispatch harness.runtime (Cancel task cancelled) - void $ await "task cancellation action" cancelled - void $ await "running task cancellation" running.settled - awaitCounts harness { running: 0, queued: 0 } - shouldNotHaveStarted queued - - it "same-key definitions with the same strategy intentionally share a slot" $ withHarness \harness -> do - first <- liftEffect makeGate - second <- liftEffect makeGate - let - firstDefinition = Halo.enqueue Search work - secondDefinition = Halo.enqueue Search work - - liftEffect do - dispatch harness.runtime (Perform firstDefinition { value: 1, gate: first }) - dispatch harness.runtime (Perform secondDefinition { value: 2, gate: second }) - void $ await "shared slot first task" first.started - awaitCounts harness { running: 1, queued: 1 } - activity <- liftEffect $ Ref.read harness.activity - Halo.activity firstDefinition activity `shouldEqual` Halo.activity secondDefinition activity - release first - void $ await "shared slot second task" second.started - release second - void $ await "shared slot completion" second.settled - - it "rejects a conflicting same-key strategy through onError" $ withHarness \harness -> do - running <- liftEffect makeGate - rejected <- liftEffect makeGate - let - established = Halo.enqueue Search work - conflicting = Halo.restartable Search work - - liftEffect do - dispatch harness.runtime (Perform established { value: 1, gate: running }) - dispatch harness.runtime (Perform conflicting { value: 2, gate: rejected }) - void $ await "established task start" running.started - void $ await "configuration error" harness.errorRaised - shouldNotHaveStarted rejected - errors <- liftEffect $ Ref.read harness.errors - errors `shouldEqual` - [ "task configuration Search: Task key was already defined as enqueue and cannot also be defined as restartable" ] - release running - - it "remembers a key's strategy across deactivate and reactivate" $ withHarness \harness -> do - first <- liftEffect makeGate - conflictingGate <- liftEffect makeGate - let - established = Halo.concurrent Search work - conflicting = Halo.drop Search work - - liftEffect $ dispatch harness.runtime (Perform established { value: 1, gate: first }) - void $ await "task before deactivation" first.started - liftEffect $ deactivate harness.runtime - void $ await "deactivated task cancellation" first.settled - liftEffect do - activate harness.runtime - dispatch harness.runtime (Perform conflicting { value: 2, gate: conflictingGate }) - void $ await "remembered configuration error" harness.errorRaised - shouldNotHaveStarted conflictingGate diff --git a/test/Test/Halo/ScopeHandlerSpec.purs b/test/Test/Halo/ScopeHandlerSpec.purs index ddfc0b7..553b808 100644 --- a/test/Test/Halo/ScopeHandlerSpec.purs +++ b/test/Test/Halo/ScopeHandlerSpec.purs @@ -3,8 +3,10 @@ module Test.Halo.ScopeHandlerSpec (spec) where import Prelude import Control.Monad.State (modify_) +import Data.Maybe (Maybe(..)) import Data.Tuple (Tuple(..)) import Effect (Effect) +import Effect.Aff (Aff) import Effect.Aff as Aff import Effect.Aff.AVar as AVar import Effect.Aff.Class (liftAff) @@ -12,70 +14,94 @@ import Effect.AVar (AVar) import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Ref as Ref -import React.Halo as Halo import React.Halo.Handlers (defaultHandlers) -import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, fork, getProps, syncSpec, updateProps) -import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, makeGate, release, shouldNotHaveStarted, withHarness, work) +import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, fork, getProps, kill, syncSpec, updateProps) +import React.Halo.Internal.Types (ForkId) +import Test.Halo.Helpers (Gate, await, makeGate, release, shouldNotHaveStarted, waitForGate) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) +identityAff :: Aff ~> Aff +identityAff = identity + spec :: Spec Unit -spec = describe "scope and handlers" do - it "cancels explicit tasks on deactivation and accepts work after reactivation" $ withHarness \harness -> do - running <- liftEffect makeGate - queued <- liftEffect makeGate - ignored <- liftEffect makeGate +spec = describe "scope, handlers, and component-owned forks" do + it "cancels roots on deactivation and accepts work after reactivation" do + forkGate <- liftEffect makeGate + forkId <- liftEffect EffectAVar.empty + handlerDone <- liftEffect EffectAVar.empty + pulseDone <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + runtime <- liftEffect $ + ( createRuntime identityAff + { initialProps: unit + , initialState: 0 + , spec: + { handlers: defaultHandlers + { onAction = case _ of + StartScopedFork gate fid completed -> do + child <- fork do + liftAff $ waitForGate gate + modify_ (_ + 1) + liftAff $ void $ AVar.tryPut child fid + liftAff $ void $ AVar.tryPut unit completed + Pulse completed -> do + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit completed + } + , onError: \_ _ -> pure unit + } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Unit Int ScopeAction Aff) + ) - let task = Halo.enqueue Save work liftEffect do - dispatch harness.runtime (Perform task { value: 1, gate: running }) - dispatch harness.runtime (Perform task { value: 2, gate: queued }) - void $ await "running task before deactivation" running.started - awaitCounts harness { running: 1, queued: 1 } + activate runtime + dispatch runtime (StartScopedFork forkGate forkId handlerDone) + void $ await "fork id" forkId + void $ await "launching handler completion" handlerDone + void $ await "fork before deactivation" forkGate.started - liftEffect $ deactivate harness.runtime - void $ await "running task cancellation on deactivation" running.settled - awaitCounts harness { running: 0, queued: 0 } - shouldNotHaveStarted queued + liftEffect $ deactivate runtime + void $ await "fork cancellation on deactivation" forkGate.settled + valueAfterDeactivate <- liftEffect $ Ref.read state + valueAfterDeactivate `shouldEqual` 0 - let ignoredTask = Halo.concurrent Search work - liftEffect $ dispatch harness.runtime (Perform ignoredTask { value: 3, gate: ignored }) + ignored <- liftEffect makeGate + ignoredId <- liftEffect EffectAVar.empty + ignoredDone <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (StartScopedFork ignored ignoredId ignoredDone) shouldNotHaveStarted ignored - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [] - reactivated <- liftEffect makeGate liftEffect do - activate harness.runtime - dispatch harness.runtime (Perform ignoredTask { value: 4, gate: reactivated }) - void $ await "task start after reactivation" reactivated.started - release reactivated - void $ await "task completion after reactivation" reactivated.settled - awaitCounts harness { running: 0, queued: 0 } - reactivatedState <- liftEffect $ Ref.read harness.state - reactivatedState `shouldEqual` [ 4 ] + activate runtime + dispatch runtime (Pulse pulseDone) + void $ await "action after reactivation" pulseDone + valueAfterReactivate <- liftEffect $ Ref.read state + valueAfterReactivate `shouldEqual` 10 + liftEffect $ deactivate runtime it "models StrictMode setup-cleanup-setup with repeatable onActivate" do activation <- liftEffect EffectAVar.empty + pulse <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new 0 runtime <- liftEffect $ - ( createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit + ( createRuntime identityAff + { initialProps: unit , initialState: 0 , spec: { handlers: defaultHandlers { onActivate = do modify_ (_ + 1) liftAff $ void $ AVar.tryPut unit activation - , onAction = \(Pulse completed) -> do + , onAction = \(ReplayPulse completed) -> do modify_ (_ + 10) liftAff $ void $ AVar.tryPut unit completed } , onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state - } :: Effect (Runtime Unit Int ReplayAction Unit) + } :: Effect (Runtime Unit Int ReplayAction Aff) ) Aff.finally (liftEffect $ deactivate runtime) do @@ -88,19 +114,16 @@ spec = describe "scope and handlers" do deactivate runtime activate runtime void $ await "StrictMode replay activation" activation - - pulse <- liftEffect EffectAVar.empty - liftEffect $ dispatch runtime (Pulse pulse) + liftEffect $ dispatch runtime (ReplayPulse pulse) void $ await "action after StrictMode replay" pulse second <- liftEffect $ Ref.read state second `shouldEqual` 12 - it "passes previous props and exposes current props to onPropsChange" do + it "passes previous props and exposes current props" do changed <- liftEffect EffectAVar.empty runtime <- liftEffect $ - ( createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: 0 + ( createRuntime identityAff + { initialProps: 0 , initialState: unit , spec: { handlers: defaultHandlers @@ -111,7 +134,7 @@ spec = describe "scope and handlers" do , onError: \_ _ -> pure unit } , stateUpdate: \_ -> pure unit - } :: Effect (Runtime Int Unit Unit Unit) + } :: Effect (Runtime Int Unit Unit Aff) ) Aff.finally (liftEffect $ deactivate runtime) do @@ -125,164 +148,163 @@ spec = describe "scope and handlers" do gate <- liftEffect makeGate state <- liftEffect $ Ref.new 0 runtime <- liftEffect $ - ( createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: 0 + ( createRuntime identityAff + { initialProps: 0 , initialState: 0 , spec: { handlers: defaultHandlers { onPropsChange = \_ -> do - runIntGate gate + liftAff $ waitForGate gate modify_ (_ + 1) } , onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state - } :: Effect (Runtime Int Int Unit Unit) + } :: Effect (Runtime Int Int Unit Aff) ) - Aff.finally (liftEffect $ deactivate runtime) do - liftEffect do - activate runtime - updateProps runtime 1 - void $ await "props-change handler start" gate.started - liftEffect $ deactivate runtime - void $ await "props-change handler cancellation" gate.settled - value <- liftEffect $ Ref.read state - value `shouldEqual` 0 + liftEffect do + activate runtime + updateProps runtime 1 + void $ await "props-change handler start" gate.started + liftEffect $ deactivate runtime + void $ await "props-change handler cancellation" gate.settled + value <- liftEffect $ Ref.read state + value `shouldEqual` 0 - it "cancels a structured fork when its action handler finishes" do + it "lets a component-owned fork outlive its launching handler" do child <- liftEffect makeGate + forkId <- liftEffect EffectAVar.empty handlerDone <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new 0 runtime <- liftEffect $ - ( createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit + ( createRuntime identityAff + { initialProps: unit , initialState: 0 , spec: { handlers: defaultHandlers - { onAction = \(ForkAndReturn gate completed) -> do - void $ fork do - runIntGate gate - modify_ (_ + 100) - liftAff $ void $ AVar.take gate.started + { onAction = \(LaunchChild gate fid completed) -> do + childId <- fork do + liftAff $ waitForGate gate + modify_ (_ + 1) + liftAff $ void $ AVar.tryPut childId fid liftAff $ void $ AVar.tryPut unit completed } , onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state - } :: Effect (Runtime Unit Int ForkAction Unit) + } :: Effect (Runtime Unit Int ForkAction Aff) ) Aff.finally (liftEffect $ deactivate runtime) do liftEffect do activate runtime - dispatch runtime (ForkAndReturn child handlerDone) - void $ await "action handler return" handlerDone - void $ await "structured child cancellation" child.settled + dispatch runtime (LaunchChild child forkId handlerDone) + void $ await "child start" child.started + void $ await "launching handler return" handlerDone + settledBeforeRelease <- liftEffect $ EffectAVar.tryTake child.settled + settledBeforeRelease `shouldEqual` Nothing + + release child + void $ await "child completion" child.settled value <- liftEffect $ Ref.read state - value `shouldEqual` 0 + value `shouldEqual` 1 - it "commit-fences a task and its structured child when replaced" do - firstParent <- liftEffect makeGate - firstChild <- liftEffect makeGate - replacement <- liftEffect makeGate - replacementDone <- liftEffect EffectAVar.empty + it "kill fences commits and capabilities, and waits for finalizers" do + child <- liftEffect makeGate + ignored <- liftEffect makeGate + forkId <- liftEffect EffectAVar.empty + launchDone <- liftEffect EffectAVar.empty + killDone <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new 0 - runtime <- liftEffect $ createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit - , initialState: 0 - , spec: - { handlers: defaultHandlers - { onAction = case _ of - ParentTask parent child -> Halo.perform parentTask (ParentWork parent child) - ReplacementTask gate completed -> Halo.perform parentTask (ReplacementWork gate completed) + runtime <- liftEffect $ + ( createRuntime identityAff + { initialProps: unit + , initialState: 0 + , spec: + { handlers: defaultHandlers + { onAction = case _ of + LaunchCancellable gate ignoredGate fid completed -> do + childId <- fork do + liftAff $ Aff.catchError (waitForGate gate) (\_ -> pure unit) + void $ fork $ liftAff $ waitForGate ignoredGate + modify_ (_ + 1) + liftAff $ void $ AVar.tryPut childId fid + liftAff $ void $ AVar.tryPut unit completed + KillChild fid completed -> do + kill fid + liftAff $ void $ AVar.tryPut unit completed + } + , onError: \_ _ -> pure unit } - , onError: \_ _ -> pure unit - } - , stateUpdate: flip Ref.write state - } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Unit Int CancelAction Aff) + ) Aff.finally (liftEffect $ deactivate runtime) do liftEffect do activate runtime - dispatch runtime (ParentTask firstParent firstChild) - void $ await "parent task start" firstParent.started - - liftEffect $ dispatch runtime (ReplacementTask replacement replacementDone) - void $ await "replaced parent task cancellation" firstParent.settled - void $ await "replaced structured child cancellation" firstChild.settled - void $ await "replacement task start" replacement.started - release replacement - void $ await "replacement task completion" replacementDone + dispatch runtime (LaunchCancellable child ignored forkId launchDone) + fid <- await "cancellable fork id" forkId + void $ await "cancellable fork launch" launchDone + void $ await "cancellable fork start" child.started + liftEffect $ dispatch runtime (KillChild fid killDone) + void $ await "kill completion" killDone + finalizerRan <- liftEffect $ EffectAVar.tryTake child.settled + finalizerRan `shouldEqual` Just unit + shouldNotHaveStarted ignored value <- liftEffect $ Ref.read state - value `shouldEqual` 10 + value `shouldEqual` 0 - it "uses the latest handlers after the hook spec changes" $ withHarness \harness -> do - gate <- liftEffect makeGate - liftEffect do - syncSpec harness.runtime - { activityUpdate: \next -> do - Ref.write next harness.activity - void $ EffectAVar.tryPut unit harness.activityChanged - , spec: - { handlers: defaultHandlers - { onAction = case _ of - Direct value workGate -> do - liftAff $ void $ AVar.tryPut unit workGate.launched - liftAff do - AVar.put unit workGate.started - void $ AVar.take workGate.release - modify_ (flip append [ value * 10 ]) - liftAff $ void $ AVar.tryPut unit workGate.settled - _ -> pure unit - } - , onError: \_ _ -> pure unit - } - , stateUpdate: flip Ref.write harness.state + it "uses the latest handlers for new actions" do + oldCompleted <- liftEffect EffectAVar.empty + newCompleted <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + let + oldHandlers = defaultHandlers + { onAction = \Refresh -> do + modify_ (_ + 1) + liftAff $ void $ AVar.tryPut unit oldCompleted } - dispatch harness.runtime (Direct 2 gate) - - void $ await "action using replacement handler" gate.started - release gate - void $ await "replacement handler completion" gate.settled - awaitCounts harness { running: 0, queued: 0 } - state <- liftEffect $ Ref.read harness.state - state `shouldEqual` [ 20 ] + newHandlers = defaultHandlers + { onAction = \Refresh -> do + modify_ (_ + 10) + liftAff $ void $ AVar.tryPut unit newCompleted + } + runtime <- liftEffect $ + ( createRuntime identityAff + { initialProps: unit + , initialState: 0 + , spec: { handlers: oldHandlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Unit Int RefreshAction Aff) + ) -data ReplayAction = Pulse (AVar Unit) + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + syncSpec runtime identityAff + { spec: { handlers: newHandlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write state + } + dispatch runtime Refresh + void $ await "new action handler" newCompleted + oldRan <- liftEffect $ EffectAVar.tryTake oldCompleted + oldRan `shouldEqual` Nothing + value <- liftEffect $ Ref.read state + value `shouldEqual` 10 -data ForkAction = ForkAndReturn Gate (AVar Unit) +data ScopeAction + = StartScopedFork Gate (AVar ForkId) (AVar Unit) + | Pulse (AVar Unit) -data ParentAction - = ParentTask Gate Gate - | ReplacementTask Gate (AVar Unit) +data ReplayAction = ReplayPulse (AVar Unit) -data ParentInput - = ParentWork Gate Gate - | ReplacementWork Gate (AVar Unit) +data ForkAction = LaunchChild Gate (AVar ForkId) (AVar Unit) -parentTask :: Halo.Task Unit Int ParentAction Unit ParentInput -parentTask = Halo.restartable unit case _ of - ParentWork parent child -> do - void $ fork do - runIntGate child - modify_ (_ + 100) - liftAff $ void $ AVar.take child.started - runIntGate parent - modify_ (_ + 1) - ReplacementWork gate completed -> do - runIntGate gate - modify_ (_ + 10) - liftAff $ void $ AVar.tryPut unit completed +data CancelAction + = LaunchCancellable Gate Gate (AVar ForkId) (AVar Unit) + | KillChild ForkId (AVar Unit) -runIntGate :: forall props action key. Gate -> HaloM props Int action key Unit -runIntGate gate = do - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release +data RefreshAction = Refresh diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index f9fabd1..e81a115 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -2,9 +2,11 @@ module Test.Halo.SubscriptionErrorSpec (spec) where import Prelude -import Control.Monad.State (get, put) +import Control.Monad.State (get, modify_, put) import Data.Foldable (traverse_) import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Aff (Aff) import Effect.Aff as Aff import Effect.Aff.AVar as AVar import Effect.Aff.Class (liftAff) @@ -13,15 +15,17 @@ import Effect.AVar as EffectAVar import Effect.Class (liftEffect) import Effect.Exception as Exception import Effect.Ref as Ref -import React.Halo as Halo import React.Halo.Handlers (Handlers, defaultHandlers) -import React.Halo.Internal.Runtime (activate, createRuntime, deactivate, dispatch, subscribe, syncSpec, unsubscribe) -import React.Halo.Internal.Types (ErrorContext(..), SubscriptionId) +import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, fork, subscribe, syncSpec, unsubscribe) +import React.Halo.Internal.Types (ErrorContext(..), ForkId, SubscriptionId) import React.Halo.Subscription (Emitter, makeEmitter) -import Test.Halo.Helpers (Action(..), Gate, Key(..), await, awaitCounts, handlers, makeGate, withHarness) +import Test.Halo.Helpers (Gate, await, makeGate, release, waitForGate) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) +identityAff :: Aff ~> Aff +identityAff = identity + spec :: Spec Unit spec = describe "subscriptions and errors" do it "removes a manual unsubscribe from scope tracking" do @@ -31,9 +35,8 @@ spec = describe "subscriptions and errors" do state <- liftEffect $ Ref.new Nothing let emitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleanupCount - runtime <- liftEffect $ createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit + runtime <- liftEffect $ createRuntime identityAff + { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state @@ -59,9 +62,8 @@ spec = describe "subscriptions and errors" do state <- liftEffect $ Ref.new Nothing let emitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleanupCount - runtime <- liftEffect $ createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit + runtime <- liftEffect $ createRuntime identityAff + { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } , stateUpdate: flip Ref.write state @@ -87,9 +89,8 @@ spec = describe "subscriptions and errors" do badEmitter = makeEmitter \_ -> pure $ Exception.throw "cleanup failed" goodEmitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleaned - runtime <- liftEffect $ createRuntime - { activityUpdate: \_ -> pure unit - , initialProps: unit + runtime <- liftEffect $ createRuntime identityAff + { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers @@ -117,59 +118,110 @@ spec = describe "subscriptions and errors" do errors <- liftEffect $ Ref.read cleanupErrors errors `shouldEqual` [ "cleanup failed" ] - it "routes an unexpected action failure with ActionError" $ withHarness \harness -> do - gate <- liftEffect makeGate - liftEffect $ dispatch harness.runtime (Boom gate) - void $ await "action error handler" harness.errorRaised + it "rejects a callback retained by a stale activation" do + callback <- liftEffect $ Ref.new Nothing + registered <- liftEffect EffectAVar.empty + state <- liftEffect $ Ref.new 0 + let + emitter = makeEmitter \receive -> do + Ref.write (Just receive) callback + pure (pure unit) + handlers = defaultHandlers + { onAction = case _ of + Register completed -> do + void $ subscribe emitter + liftAff $ void $ AVar.tryPut unit completed + Ping -> modify_ (_ + 1) + } + runtime <- liftEffect $ + ( createRuntime identityAff + { initialProps: unit + , initialState: 0 + , spec: { handlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write state + } :: Effect (Runtime Unit Int StaleAction Aff) + ) - errors <- liftEffect $ Ref.read harness.errors - errors `shouldEqual` [ "action: boom" ] + liftEffect do + activate runtime + dispatch runtime (Register registered) + void $ await "stale callback registration" registered + retained <- liftEffect $ Ref.read callback + liftEffect do + deactivate runtime + activate runtime + traverse_ (\receive -> receive Ping) retained + value <- liftEffect $ Ref.read state + value `shouldEqual` 0 + liftEffect $ deactivate runtime - it "uses the latest unexpected-error callback after a spec change" $ withHarness \harness -> do - replacementErrors <- liftEffect $ Ref.new [] - replacementRaised <- liftEffect EffectAVar.empty + it "routes action failures and uses the latest onError callback" do gate <- liftEffect makeGate - liftEffect do - syncSpec harness.runtime - { activityUpdate: \next -> do - Ref.write next harness.activity - void $ EffectAVar.tryPut unit harness.activityChanged - , spec: - { handlers + oldErrors <- liftEffect $ Ref.new [] + newErrors <- liftEffect $ Ref.new [] + newRaised <- liftEffect EffectAVar.empty + runtime <- liftEffect $ makeErrorRuntime oldErrors + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (Boom gate) + void $ await "failing action start" gate.started + + liftEffect $ syncSpec runtime identityAff + { spec: + { handlers: errorHandlers , onError: \context error -> do let label = case context of - ActionError _ -> "replacement action" - _ -> "wrong replacement context" - Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) replacementErrors - void $ EffectAVar.tryPut unit replacementRaised + ActionError (Boom _) -> "action" + _ -> "wrong context" + Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) newErrors + void $ EffectAVar.tryPut unit newRaised } - , stateUpdate: flip Ref.write harness.state + , stateUpdate: \_ -> pure unit } - dispatch harness.runtime (Boom gate) + release gate + void $ await "latest action error callback" newRaised - void $ await "replacement error callback" replacementRaised - oldErrors <- liftEffect $ Ref.read harness.errors - oldErrors `shouldEqual` [] - newErrors <- liftEffect $ Ref.read replacementErrors - newErrors `shouldEqual` [ "replacement action: boom" ] + previous <- liftEffect $ Ref.read oldErrors + current <- liftEffect $ Ref.read newErrors + previous `shouldEqual` [] + current `shouldEqual` [ "action: action boom" ] - it "routes an explicit task failure with its task key" $ withHarness \harness -> do + it "routes an unexpected fork failure with ForkError" do gate <- liftEffect makeGate - let - failingTask = Halo.restartable Save \_ -> - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - Aff.throwError (Aff.error "task boom") - liftEffect $ dispatch harness.runtime (PerformUnit failingTask gate) - void $ await "failing task start" gate.started - void $ await "task error handler" harness.errorRaised - awaitCounts harness { running: 0, queued: 0 } - - errors <- liftEffect $ Ref.read harness.errors - errors `shouldEqual` [ "task Save: task boom" ] + forkId <- liftEffect EffectAVar.empty + errors <- liftEffect $ Ref.new [] + raised <- liftEffect EffectAVar.empty + runtime <- liftEffect $ + ( createRuntime identityAff + { initialProps: unit + , initialState: unit + , spec: + { handlers: errorHandlers + , onError: \context error -> do + let + label = case context of + ForkError fid -> "fork " <> show fid + _ -> "wrong context" + Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) errors + void $ EffectAVar.tryPut unit raised + } + , stateUpdate: \_ -> pure unit + } :: Effect (Runtime Unit Unit ErrorAction Aff) + ) + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (ForkBoom gate forkId) + fid <- await "failing fork id" forkId + void $ await "failing fork start" gate.started + release gate + void $ await "fork error callback" raised + actual <- liftEffect $ Ref.read errors + actual `shouldEqual` [ "fork " <> show fid <> ": fork boom" ] data SubscriptionAction = Start (Emitter SubscriptionAction) (AVar Unit) @@ -178,7 +230,7 @@ data SubscriptionAction type SubscriptionState = Maybe SubscriptionId -subscriptionHandlers :: Handlers Unit SubscriptionState SubscriptionAction Unit +subscriptionHandlers :: Handlers Unit SubscriptionState SubscriptionAction Aff subscriptionHandlers = defaultHandlers { onAction = case _ of Start emitter completed -> do @@ -190,10 +242,37 @@ subscriptionHandlers = defaultHandlers traverse_ unsubscribe sid put Nothing liftAff $ void $ AVar.tryPut unit completed - Block gate -> - liftAff $ Aff.finally - (void $ AVar.tryPut unit gate.settled) - do - AVar.put unit gate.started - void $ AVar.take gate.release + Block gate -> liftAff $ waitForGate gate + } + +data StaleAction + = Register (AVar Unit) + | Ping + +data ErrorAction + = Boom Gate + | ForkBoom Gate (AVar ForkId) + +errorHandlers :: Handlers Unit Unit ErrorAction Aff +errorHandlers = defaultHandlers + { onAction = case _ of + Boom gate -> do + liftAff $ waitForGate gate + liftAff $ Aff.throwError (Aff.error "action boom") + ForkBoom gate fid -> do + child <- fork do + liftAff $ waitForGate gate + liftAff $ Aff.throwError (Aff.error "fork boom") + liftAff $ void $ AVar.tryPut child fid + } + +makeErrorRuntime :: Ref.Ref (Array String) -> Effect (Runtime Unit Unit ErrorAction Aff) +makeErrorRuntime errors = createRuntime identityAff + { initialProps: unit + , initialState: unit + , spec: + { handlers: errorHandlers + , onError: \_ error -> Ref.modify_ (_ <> [ Exception.message error ]) errors + } + , stateUpdate: \_ -> pure unit } From 3f4188a79e1525104226b9577e9d308dc5ac4fc2 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 21:24:02 +0900 Subject: [PATCH 08/16] Fence lifted effects after Halo cancellation --- README.md | 2 +- docs/guide.md | 4 +- docs/reference.md | 2 + src/React/Halo/Hook.purs | 5 +- src/React/Halo/Internal/Runtime.purs | 17 +++++-- test/Test/Halo/RuntimeSpec.purs | 70 +++++++++++++++++++++++++++- 6 files changed, 88 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a02f4dd..c7fe968 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ handlers = Halo.defaultHandlers } ``` -`lift` is `Control.Monad.Trans.Class.lift`. The `AppM` value runs through the interpreter captured when that handler or fork started. A new render may supply a new interpreter for later roots without changing one already running. +`lift` is `Control.Monad.Trans.Class.lift`. A root captures its interpreter when it starts. A fork launched later by that root inherits the same snapshot, even if a newer render has supplied another interpreter; unrelated new handlers use the latest interpreter. Create the component at the application boundary: diff --git a/docs/guide.md b/docs/guide.md index 591fc5f..0b73f38 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -92,9 +92,9 @@ startSearch query = do modify_ _ { searchFiber = Just fiber } ``` -`Halo.kill id` removes the fork from component tracking, fences its state and capabilities synchronously, requests Aff cancellation, and waits for cancellation and Aff finalizers before returning. Killing an unknown or completed ID does nothing. +`Halo.kill id` removes the fork from component tracking, fences its state and capabilities synchronously, requests Aff cancellation, and waits for cancellation and Aff finalizers before returning. Killing an unknown or completed ID does nothing. A fork inherits its launching root's application interpreter snapshot, even when a render supplies a newer interpreter before the fork starts. -Deactivation cannot wait asynchronously because React cleanup is synchronous. It fences the whole activation first, attempts every subscription cleanup, and requests cancellation of all remaining handlers and forks. Aff finalizers continue in their cancellation fibers, but they cannot commit Halo state. +Deactivation cannot wait asynchronously because React cleanup is synchronous. It fences the whole activation first, attempts every subscription cleanup, and requests cancellation of all remaining handlers and forks. Aff finalizers continue in their cancellation fibers, but they cannot commit Halo state or start a newly lifted application effect after the fence. Cancellation is cooperative. It cannot retract an HTTP request, storage write, callback, or log that already happened. Design external writes for retry and idempotency where needed. diff --git a/docs/reference.md b/docs/reference.md index c9740f5..5c424aa 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -33,6 +33,8 @@ MonadTell output m => MonadTell output (HaloM props state action m) MonadThrow error m => MonadThrow error (HaloM props state action m) ``` +Each `lift` checks the root fence before invoking the captured interpreter. A stale root therefore cannot start a new application effect, even when it catches its initial Aff cancellation. + `HaloAp` is the abstract parallel applicative counterpart: ```purescript diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index 6910657..468d675 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -48,8 +48,9 @@ derive instance newtypeUseHalo :: Newtype (UseHalo props state action m hooks) _ -- | Run Halo inside a `react-basic-hooks` component. -- | -- | The natural transformation interprets application effects in `m` into the --- | `Aff` fibers owned by the active React scope. New roots use the latest --- | interpreter; roots already running retain their starting snapshot. +-- | `Aff` fibers owned by the active React scope. New handlers use the latest +-- | interpreter; roots already running retain their starting snapshot, and a +-- | fork inherits the snapshot of the root that launches it. useHalo :: forall props state action m . (m ~> Aff) diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index 448de33..d9fed6d 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -71,9 +71,12 @@ derive newtype instance applyHaloAp :: Apply (HaloAp props state action m) derive newtype instance applicativeHaloAp :: Applicative (HaloAp props state action m) instance monadTransHaloM :: MonadTrans (HaloM props state action) where - lift value = HaloM $ ReaderT \execution -> - case execution.runInAff of - RunInAff run -> run value + lift value = HaloM $ ReaderT \execution -> do + current <- liftEffect $ isCurrent execution + if current then + case execution.runInAff of + RunInAff run -> run value + else Aff.throwError scopeCancellationError -- Public effect capabilities deliberately pass through the application monad. instance monadEffectHaloM :: MonadEffect m => MonadEffect (HaloM props state action m) where @@ -296,7 +299,8 @@ getProps = HaloM $ ReaderT \execution -> do liftEffect $ Ref.read runtime.props -- | Start work owned by the current React activation. The fork may outlive its --- | launching handler and is cancelled on explicit `kill` or deactivation. +-- | launching handler, inherits that root's interpreter snapshot, and is +-- | cancelled on explicit `kill` or deactivation. fork :: forall props state action m . HaloM props state action m Unit @@ -456,7 +460,10 @@ requestCancel root = Aff.launchAff_ (cancelRootAff root) cancelRootAff :: Root -> Aff Unit cancelRootAff root@(Root current) = do liftEffect $ fenceRoot root - Aff.killFiber (Aff.error "Halo scope cancelled") current.fiber + Aff.killFiber scopeCancellationError current.fiber + +scopeCancellationError :: Error +scopeCancellationError = Aff.error "Halo scope cancelled" isCurrent :: forall props state action m diff --git a/test/Test/Halo/RuntimeSpec.purs b/test/Test/Halo/RuntimeSpec.purs index e0906f0..8e71d29 100644 --- a/test/Test/Halo/RuntimeSpec.purs +++ b/test/Test/Halo/RuntimeSpec.purs @@ -2,7 +2,7 @@ module Test.Halo.RuntimeSpec (spec) where import Prelude -import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.Reader (ReaderT(..), ask, runReaderT) import Control.Monad.State (modify_) import Control.Monad.Trans.Class (lift) import Control.Parallel (parallel, sequential) @@ -16,7 +16,8 @@ import Effect.AVar as EffectAVar import Effect.Class (class MonadEffect, liftEffect) import Effect.Ref as Ref import React.Halo.Handlers (Handlers, defaultHandlers) -import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, syncSpec) +import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, fork, kill, syncSpec) +import React.Halo.Internal.Types (ForkId) import Test.Halo.Helpers (Gate, await, makeGate, release, waitForGate) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) @@ -37,8 +38,19 @@ runAppM environment (AppM computation) = runReaderT computation environment readEnvironment :: AppM Int readEnvironment = AppM ask +catchGateCancellation :: Gate -> AppM Unit +catchGateCancellation gate = AppM $ ReaderT \_ -> + Aff.catchError (waitForGate gate) (\_ -> pure unit) + +writeExternalWitness :: Ref.Ref Boolean -> AppM Unit +writeExternalWitness witness = AppM $ ReaderT \_ -> + liftEffect $ Ref.write true witness + data Action = ReadEnvironment Gate (AVar Int) + | LaunchSnapshotFork Gate Gate (AVar Int) + | LaunchCancellableLift Gate (Ref.Ref Boolean) (AVar ForkId) (AVar Unit) + | KillLift ForkId (AVar Unit) | RunParallel Gate Gate (AVar Unit) type State = Int @@ -50,6 +62,21 @@ handlers = defaultHandlers liftAff $ waitForGate gate environment <- lift readEnvironment liftAff $ void $ AVar.tryPut environment completed + LaunchSnapshotFork handlerGate childGate completed -> do + liftAff $ waitForGate handlerGate + void $ fork do + liftAff $ waitForGate childGate + environment <- lift readEnvironment + liftAff $ void $ AVar.tryPut environment completed + LaunchCancellableLift gate witness forkId completed -> do + childId <- fork do + lift $ catchGateCancellation gate + lift $ writeExternalWitness witness + liftAff $ void $ AVar.tryPut childId forkId + liftAff $ void $ AVar.tryPut unit completed + KillLift childId completed -> do + kill childId + liftAff $ void $ AVar.tryPut unit completed RunParallel left right completed -> do Tuple a b <- sequential ado a <- parallel do @@ -107,6 +134,45 @@ spec = describe "application monad and parallelism" do first `shouldEqual` 1 second `shouldEqual` 2 + it "gives a later fork its launching handler's interpreter snapshot" do + { runtime, state } <- makeRuntime 1 + Aff.finally (liftEffect $ deactivate runtime) do + handlerGate <- liftEffect makeGate + childGate <- liftEffect makeGate + childResult <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (LaunchSnapshotFork handlerGate childGate childResult) + void $ await "snapshot handler start" handlerGate.started + + liftEffect $ syncSpec runtime (runAppM 2) + { spec: { handlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write state + } + + release handlerGate + void $ await "snapshot child start" childGate.started + release childGate + result <- await "snapshot child environment" childResult + result `shouldEqual` 1 + + it "rejects a freshly lifted AppM effect after kill fences its fork" do + { runtime } <- makeRuntime 1 + Aff.finally (liftEffect $ deactivate runtime) do + gate <- liftEffect makeGate + witness <- liftEffect $ Ref.new false + forkId <- liftEffect EffectAVar.empty + launched <- liftEffect EffectAVar.empty + killed <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (LaunchCancellableLift gate witness forkId launched) + childId <- await "cancellable AppM fork id" forkId + void $ await "cancellable AppM fork launch" launched + void $ await "cancellable AppM effect start" gate.started + + liftEffect $ dispatch runtime (KillLift childId killed) + void $ await "cancellable AppM fork kill" killed + void $ await "cancelled AppM effect settlement" gate.settled + changed <- liftEffect $ Ref.read witness + changed `shouldEqual` false + it "runs Halo branches concurrently with the direct Parallel instance" do { runtime, state } <- makeRuntime 1 Aff.finally (liftEffect $ deactivate runtime) do From ac61d23b173236fcddbae18ef4dcbf6d202c4f06 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 21:27:18 +0900 Subject: [PATCH 09/16] Clarify fork interpreter inheritance --- docs/reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.md b/docs/reference.md index 5c424aa..90f1f46 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -167,7 +167,7 @@ useHalo (HaloResult state action) ``` -The natural transformation is captured for each new handler or fork root. Updating the hook with another interpreter affects later roots only. Cleanup deactivates the current scope; StrictMode reactivation creates a fresh usable scope. +Each new handler captures the latest natural transformation supplied to the hook. A fork inherits the transformation captured by the root that launches it, even if a newer render supplied another interpreter before the fork starts. Cleanup deactivates the current scope; StrictMode reactivation creates a fresh usable scope. ## Component API From 804fc2c81a44c9acdcfbf8eb59ec5d41110f0731 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Tue, 1 Sep 2026 22:21:08 +0900 Subject: [PATCH 10/16] Build coherent Halo documentation --- AGENTS.md | 35 +++++ CONTRIBUTING.md | 55 ++++++++ README.md | 73 +++++----- docs/architecture.md | 88 ++++++++++++ docs/guide.md | 108 ++++++++++----- docs/reference.md | 195 --------------------------- src/React/Halo.purs | 9 ++ src/React/Halo/Component.purs | 10 +- src/React/Halo/Handlers.purs | 13 +- src/React/Halo/Hook.purs | 22 ++- src/React/Halo/Internal/Runtime.purs | 63 ++++++--- src/React/Halo/Internal/Types.purs | 14 +- src/React/Halo/Subscription.purs | 14 +- test/Test/Halo/DocExamples.purs | 46 +------ test/Test/Halo/GuideExamples.purs | 32 +++++ 15 files changed, 431 insertions(+), 346 deletions(-) create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/architecture.md delete mode 100644 docs/reference.md create mode 100644 test/Test/Halo/GuideExamples.purs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f753a11 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# Repository instructions for agents + +Use this file as the repository control plane. Do not treat it as a substitute for the reader and maintainer documentation. + +## Read the relevant sources first + +For an unfamiliar task, route yourself by scope: + +- Read [README.md](README.md) for the product boundary and current public mental model. +- Read [docs/guide.md](docs/guide.md) for supported usage and cancellation guidance. +- Read [docs/architecture.md](docs/architecture.md) before changing runtime ownership, interpreters, concurrency, subscriptions, or error handling. +- Read [CONTRIBUTING.md](CONTRIBUTING.md) for setup, validation, and pull request readiness. +- Inspect [`React.Halo`](src/React/Halo.purs) and the public module that owns an API before changing its contract. +- Inspect the corresponding modules under `test/Test/Halo/` before changing behavior; tests are executable contracts for runtime invariants. + +Before changing dependencies or developer commands, inspect `package.json`, `spago.yaml`, and `.github/workflows/ci.yml`. `package.json` owns tool pins and scripts; `spago.yaml` owns PureScript dependencies and the package set; CI owns automated pull request checks. + +## Preserve repository boundaries + +- Keep `React.Halo` as the intentional public API root. Do not expose internal runtime ownership types as a shortcut. +- Keep React integration in the component/hook boundary and runtime ownership in `React.Halo.Internal.Runtime`. Read the architecture document instead of duplicating its rules here. +- Keep application capabilities routed through the application monad and its `m ~> Aff` interpreter. +- Do not add npm runtime dependencies or an npm runtime entry point. The npm package is development tooling only. +- Update public docs, compile-checked examples, and focused deterministic tests when public behavior changes. +- Do not hand-edit or commit `generated-docs/`, `output/`, `.spago/`, or `node_modules/`; they are ignored generated or dependency state. + +## Validate completion + +Use focused checks while iterating. Before declaring a repository change complete, run the full sequence in [CONTRIBUTING.md](CONTRIBUTING.md): format check, strict and pedantic build, full tests, and docs generation. Review the final diff, verify local documentation links, and run a whitespace check. Report any skipped, failed, or unavailable validation precisely. + +There is no real DOM fixture. Describe successful runtime tests and compile checks accurately; do not claim browser mounting coverage. + +## Require explicit approval for external actions + +Do not push commits, publish packages or documentation, create a release, edit GitHub or pull request state, or change any other external resource without current, action-specific authorization. Repository change approval does not imply release or publication approval. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d192a5b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +This guide covers local setup, validation, and pull request readiness. Start with the [README](README.md) for the public mental model, the [guide](docs/guide.md) for API usage, and the [architecture notes](docs/architecture.md) before changing runtime ownership or cancellation behavior. + +## Set up the checkout + +Use Node.js 22 with npm to match CI, then install the locked development tools: + +```console +npm ci +``` + +`package.json` is the source of truth for tool pins and npm scripts. `spago.yaml` owns the PureScript dependencies and package set. CI owns the automated checks run for pull requests. + +Halo has no npm runtime entry point or npm runtime dependencies. Do not add an npm runtime dependency to provide behavior that belongs in PureScript or an existing PureScript package. Add or change PureScript dependencies in `spago.yaml`. + +## Develop and validate + +During implementation, run the smallest relevant build or test that gives useful feedback. Changes to runtime ownership, cancellation, subscriptions, interpreters, or public behavior need focused regression coverage in the corresponding test module. + +Before marking a pull request ready, run the full local validation sequence from the repository root: + +```console +npm run format:check +npm run build -- --strict --pedantic-packages +npm test +npx spago docs +``` + +Use `npm run format` to apply the repository formatter when the format check fails. A focused check helps iteration but does not replace the full sequence before review. + +The documentation command writes generated API pages to `generated-docs/`. Spago also writes build and dependency state to `output/` and `.spago/`. These paths are ignored; do not hand-edit or commit them. + +## Keep behavior, tests, and docs aligned + +`React.Halo` is the public API root. When a change affects its exported types or behavior: + +- update the relevant public module documentation; +- update the README or guide when their guidance changes, and keep exact API contracts in public source comments used by generated documentation; +- keep examples in `test/Test/Halo/DocExamples.purs` compiling; and +- add or update deterministic tests for the changed invariant. + +Use [docs/architecture.md](docs/architecture.md) to find the runtime contract and the tests that protect it. The runtime tests exercise ownership directly without a real DOM fixture, so report validation as runtime or compile coverage rather than as a mounted-browser test. + +Documentation-only changes still require link inspection and documentation generation. Run the complete validation sequence when preparing the pull request so CI-facing code, examples, and package checks remain covered. + +## Check pull request readiness + +Before requesting review, confirm that: + +- the change is focused and its public effect is clear; +- formatting, strict and pedantic build, full tests, and docs generation pass; +- public API changes include matching documentation and tests; +- generated or dependency output is not staged; and +- the final diff contains only intended files and no whitespace errors. diff --git a/README.md b/README.md index c7fe968..2baa94a 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # React Halo -Halo gives a PureScript React component one typed action handler, component state, and a safe boundary for application effects. Define UI interactions with an action ADT, lift your application monad into `HaloM`, and supply an interpreter from that monad to `Aff` when the component or hook is created. +Halo gives a PureScript React component a typed action handler, local state, and a safe boundary for application effects. Your application logic remains in its own monad; Halo adds access to props and state, action dispatch, component-owned processes, subscriptions, and cleanup. -Each active React effect owns its handlers, component forks, and subscriptions. Deactivation cancels that work, and work that has been killed or deactivated cannot commit Halo state. +Use Halo when several UI interactions share state and asynchronous work must remain owned by the component. For a single request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. -For one request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. Halo is useful when actions, shared state transitions, application logic, and cancellable component processes need one coherent owner. +## How Halo fits -## Mental model +A Halo component has three main parts: -1. **Actions** are values in your UI action ADT. Rendering code calls `dispatch :: action -> Effect Unit`; Halo starts the action handler in the active component scope. -2. **Application effects** remain in your application monad, commonly `ReaderT AppEnv Aff`. Use the standard transformer `lift` inside `HaloM`. The interpreter supplied to `component` or `useHalo` runs those effects in Halo-owned `Aff` fibers. -3. **Forks** are component-owned processes. A fork may outlive the handler that started it, can be killed by its `ForkId`, and is cancelled when the React scope deactivates. +1. **Actions** describe UI interactions. Rendering code calls `dispatch :: action -> Effect Unit`, and Halo starts the corresponding action handler in the active component scope. +2. **Application effects** remain in an application monad such as `ReaderT Env Aff`. Standard `lift` embeds those effects in `HaloM`, and an interpreter supplied at the React boundary translates them to `Aff`. +3. **Forks** are cancellable processes owned by the active component. A fork may outlive the handler that started it, but it cannot outlive the React activation that owns it. Halo does not provide global state, server caching, or a separate process runtime. -## Try the unreleased v4 +## Install this unreleased version -Halo v4 targets PureScript 0.15.16 and Spago 1.0.4. It is not published yet; the Registry still resolves `react-halo` to v3. Add a sibling checkout as a local package: +The API documented on this branch is not published yet; the PureScript Registry currently resolves `react-halo` to v3. This branch uses the PureScript and Spago versions pinned in [`package.json`](package.json). + +Add a checkout as a local Spago package and declare the dependencies imported by the example below: ```yaml package: @@ -24,7 +26,6 @@ package: - aff - console - effect - - either - exceptions - foldable-traversable - maybe @@ -40,19 +41,21 @@ workspace: path: ../purescript-react-halo ``` -After v4 is published: +After v4 is published, the local override can be replaced with: ```console -spago install aff console effect either exceptions foldable-traversable maybe prelude react-basic-dom react-basic-hooks react-halo transformers +spago install aff console effect exceptions foldable-traversable maybe prelude react-basic-dom react-basic-hooks react-halo transformers ``` -This list is complete for the quick-start shape under Spago's pedantic dependency check; an existing React application will already declare several packages. `react-basic-dom` is required by the renderer, not by Halo. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. +`react-basic-dom` is used by this example, not required by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. ## Quick start -Define the application monad and its runtime interpreter: +Define an application monad and the interpreter that runs it: ```purescript +type Env = { loadGreeting :: Aff String } + newtype AppM a = AppM (ReaderT Env Aff a) derive newtype instance functorAppM :: Functor AppM @@ -65,11 +68,18 @@ derive newtype instance monadAffAppM :: MonadAff AppM runAppM :: Env -> AppM ~> Aff runAppM env (AppM program) = runReaderT program env + +loadGreeting :: AppM String +loadGreeting = AppM do + env <- ask + liftAff env.loadGreeting ``` -Use an action ADT for interactions and keep cancellation identity in component state: +Define component state and an action ADT. Store a `ForkId` when another action must be able to cancel the process: ```purescript +type Props = { title :: String } + type State = { fiber :: Maybe Halo.ForkId , loading :: Boolean @@ -99,9 +109,9 @@ handlers = Halo.defaultHandlers } ``` -`lift` is `Control.Monad.Trans.Class.lift`. A root captures its interpreter when it starts. A fork launched later by that root inherits the same snapshot, even if a newer render has supplied another interpreter; unrelated new handlers use the latest interpreter. +`lift` is `Control.Monad.Trans.Class.lift`. Each handler captures the interpreter current when it starts. A fork inherits its launching handler's interpreter, even if React renders with a newer interpreter before the fork begins. -Create the component at the application boundary: +Supply the interpreter when creating the component: ```purescript loadButton :: Env -> Component Props @@ -109,8 +119,8 @@ loadButton env = Halo.component "LoadButton" (runAppM env) { initialState: \_ -> { fiber: Nothing, loading: false, result: Nothing } , handlers - , onError: \context error -> - Console.error $ showContext context <> ": " <> message error + , onError: \_ error -> + Console.error $ "Unexpected Halo error: " <> message error , render: \{ props, state, dispatch } -> R.div_ [ R.text props.title @@ -122,6 +132,9 @@ loadButton env = Halo.component "LoadButton" (runAppM env) { onClick: capture_ (dispatch Cancel) , children: [ R.text "Cancel" ] } + , R.text $ case state.result of + Nothing -> if state.loading then "Loading…" else "Not loaded" + Just greeting -> greeting ] } ``` @@ -142,21 +155,13 @@ halo <- Halo.useHalo (runAppM env) -- halo.dispatch ``` -## Learn and reference +A complete version of this example is compiled as [`test/Test/Halo/DocExamples.purs`](test/Test/Halo/DocExamples.purs). -- [Guide](docs/guide.md): application monads, handlers, component ownership, cancellation, parallelism, subscriptions, and errors. -- [API reference](docs/reference.md): public types and exact runtime semantics. +## Learn more -The documentation examples are compile-checked in `test/Test/Halo/DocExamples.purs`. - -## Development - -```console -npm ci -npm run format:check -npm run build -- --strict --pedantic-packages -npm test -npx spago docs -``` +- The [Halo guide](docs/guide.md) explains actions, state, component processes, cancellation, parallelism, subscriptions, and errors. +- Generate the exact API reference from public source comments with `npx spago docs --offline`. +- The [runtime architecture](docs/architecture.md) describes ownership and cancellation invariants for maintainers. +- See [Contributing](CONTRIBUTING.md) before changing the library. -The deterministic runtime tests model React's setup-cleanup-setup sequence directly. A DOM mounting test is intentionally omitted because the package manifest contains only the pinned PureScript compiler and Spago; the hook uses the tested runtime boundary, and component examples are compile-checked. +The deterministic tests model React's setup-cleanup-setup sequence directly. The repository does not yet include a real DOM/StrictMode mounting fixture. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..afcf848 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,88 @@ +# Halo architecture + +This document describes the ownership and safety invariants that maintainers must preserve. For public usage, start with the [README](../README.md) and [guide](guide.md). + +## Application effects cross one boundary + +Halo keeps application logic in an application monad `m`, commonly an `AppM` built on `ReaderT environment Aff`. `component` and `useHalo` receive a natural transformation in one direction: + +```purescript +m ~> Aff +``` + +`HaloM props state action m` is a `ReaderT` over an internal execution record in `Aff`. Standard `lift` first checks that the execution still owns a current root, then invokes the root's captured interpreter. Derived `MonadEffect`, `MonadAff`, `MonadAsk`, `MonadTell`, and `MonadThrow` capabilities all pass through `m` and therefore use the same fence. Halo component state remains the separate `MonadState state` capability. + +The interpreter must return the owned `Aff` computation. An interpreter that detaches work with `launchAff_` moves that work outside Halo's cancellation boundary. + +## React delegates ownership to the runtime + +[`React.Halo.Component`](../src/React/Halo/Component.purs) computes initial state from the initial props and delegates to `useHalo`. The renderer receives current props, Halo state, and an action dispatcher. + +[`React.Halo.Hook`](../src/React/Halo/Hook.purs) creates one internal runtime for the hook instance and connects it to React effects: + +1. synchronize the latest interpreter, handlers, error callback, and React state setter; +2. activate the runtime and return synchronous deactivation as effect cleanup; and +3. publish prop changes to the runtime. + +The hook returns only current state and `dispatch`. [`React.Halo.Internal.Runtime`](../src/React/Halo/Internal/Runtime.purs) owns fibers, activation scopes, subscriptions, state fencing, and error routing. + +## Each activation has a generation + +An active runtime holds one scope with a unique generation, an active flag, and maps for handler roots, component forks, and subscriptions. Activation is idempotent while that scope is current. + +Deactivation marks the scope inactive and clears it from the runtime before foreign cleanup or Aff cancellation begins. A later activation creates fresh maps and a new generation. Currency checks require both an active matching generation and a live root owner, so work retained from an earlier generation cannot affect a reactivated component. + +This generation boundary models React development StrictMode's setup-cleanup-setup sequence. `onActivate` runs once for every actual activation; cleanup fences the earlier generation before the replayed activation starts. + +## Handlers and forks are roots + +Every `onActivate`, `onPropsChange`, and `onAction` invocation starts an independently owned handler root. A new handler reads the current interpreter and latest handlers from the runtime. Preparation creates its owner and gated fiber; the runtime records the root before opening the gate, so immediate completion cannot race registration. + +`fork` creates another activation-owned root with an opaque `ForkId`. It has its own liveness fence and may outlive the handler that launched it. The fork inherits the launching root's interpreter snapshot, even if hook synchronization supplied a newer interpreter before the call to `fork`. Unrelated handlers started after synchronization use the newer interpreter. + +Root completion removes only that root's current map entry. IDs are fresh within the runtime, so stale completion cannot remove newer work. + +## Fences precede cancellation + +Cancellation is cooperative, but ownership loss is synchronous. + +For explicit `kill`, the runtime removes the fork from tracking, fences its owner, requests Aff cancellation, and waits for the fiber and its Aff finalizers before returning. An unknown or completed `ForkId` is a no-op. + +React deactivation cannot wait asynchronously. It invalidates the scope, takes all tracked roots and subscriptions, fences every root, attempts every synchronous subscription cleanup, and then requests cancellation of all handler and fork fibers. Cleanup failures are reported only after the runtime has attempted the rest of the cleanup work. + +The fences protect two important boundaries: + +- `MonadState` may still compute a stale operation's return value, but it cannot update stored state or call React's state setter. +- A later `lift` from a stale root fails with Halo's internal cancellation error before invoking `m ~> Aff`. Catching the initial Aff cancellation therefore cannot start a newly lifted application effect. + +Capabilities that create or remove forks and subscriptions also check currency. Cancellation cannot undo an external effect that already happened inside an application computation; application writes must still use appropriate idempotency or retry semantics. + +## Subscriptions close over their activation + +The local [`Emitter`](../src/React/Halo/Subscription.purs) registers an `Effect` callback and returns a synchronous cleanup. The scope tracks that cleanup by `SubscriptionId`. Manual unsubscription removes the entry before running cleanup, which prevents a throwing cleanup from being retried during deactivation. + +Deactivation takes the complete subscription map and attempts each cleanup independently. A retained emitter callback still dispatches through its original scope; the generation check rejects it after deactivation, including after StrictMode reactivation. + +## Parallel branches share ownership + +`HaloAp` is the direct parallel counterpart to `HaloM`: it changes the internal `ReaderT` result from `Aff` to `ParAff` without creating another ownership model. Parallel branches share one root owner, activation scope, error context, and interpreter snapshot. The surrounding computation waits for the branches when it returns to `HaloM`. + +Concurrent Halo state writes have nondeterministic ordering and can overwrite one another. Prefer running independent application reads in parallel, combining their results, and committing Halo state once. + +## Errors use the current reporting callback + +Each root carries the context assigned at launch: `ActivationError`, `PropsChangeError previousProps`, `ActionError action`, or `ForkError id`. An unexpected failure is reported only while that root and scope are still current. The runtime reads the latest `onError` callback at reporting time, so a render can update reporting without changing an already-running root's interpreter. + +Subscription cleanup failures use `DeactivationError`. Halo-initiated cancellation and stale-lift failures are suppressed because the owner has already been fenced. + +## Tests protect the invariant boundaries + +The deterministic runtime tests exercise ownership without mounting a real DOM fixture: + +- [`RuntimeSpec`](../test/Test/Halo/RuntimeSpec.purs) covers AppM interpretation, handler and fork interpreter snapshots, the stale-lift fence, and direct parallel execution. +- [`ScopeHandlerSpec`](../test/Test/Halo/ScopeHandlerSpec.purs) covers activation generations, StrictMode reactivation, props, handler and fork ownership, explicit kill, finalizer waiting, and stale capability/state rejection. +- [`SubscriptionErrorSpec`](../test/Test/Halo/SubscriptionErrorSpec.purs) covers cleanup isolation, stale emitter callbacks, error contexts, and latest error-handler selection. +- [`DocExamples`](../test/Test/Halo/DocExamples.purs) compile-checks the complete component, hook, AppM, and fork example. +- [`GuideExamples`](../test/Test/Halo/GuideExamples.purs) compile-checks the guide's parallel and subscription examples. + +[`test/Main.purs`](../test/Main.purs) runs the full behavioral suite. Preserve these deterministic boundaries when changing the runtime; add a focused regression beside the invariant it protects. diff --git a/docs/guide.md b/docs/guide.md index 0b73f38..0bbc2d3 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,10 +1,10 @@ -# Halo v4 guide +# Halo guide -Halo combines a typed UI action handler with component state and a runtime boundary for your application monad. This guide starts from the [README quick start](../README.md); use the [API reference](reference.md) for exact signatures. +Halo combines a typed UI action handler with component state and a runtime boundary for your application monad. Start with the [README quick start](../README.md), then use this guide when choosing lifetimes, cancellation, or cleanup behavior. -## Keep application logic in `AppM` +## Run application logic through `AppM` -Most applications already have a monad that carries services or configuration over `Aff`: +An application monad commonly carries services or configuration over `Aff`: ```purescript newtype AppM a = AppM (ReaderT AppEnv Aff a) @@ -13,13 +13,13 @@ runAppM :: AppEnv -> AppM ~> Aff runAppM env (AppM program) = runReaderT program env ``` -Halo restores that monad as the fourth `HaloM` parameter: +Use it as the fourth `HaloM` parameter: ```purescript -HaloM props state action AppM result +type UI a = HaloM Props State Action AppM a ``` -Use the standard transformer operation to run application logic: +Standard transformer `lift` runs application logic inside a Halo computation: ```purescript import Control.Monad.Trans.Class (lift) @@ -30,16 +30,18 @@ loadAccount = do modify_ _ { account = Just account } ``` -The interpreter is explicit at the React boundary: +Supply the interpreter at the React boundary: ```purescript Halo.component "Account" (runAppM env) spec Halo.useHalo (runAppM env) hookSpec ``` -A handler or fork keeps the interpreter with which it started. If a later render supplies another interpreter, only new roots use it. Do not implement an interpreter by detaching work with `launchAff_`: Halo can only own and cancel the `Aff` returned by the interpreter. +Each new handler captures the latest interpreter supplied to the hook. A fork inherits the interpreter captured by the root that launches it. This keeps one running action on one application environment even when a later render supplies another interpreter. -## Handle an action ADT +The interpreter must return the `Aff` that performs the work. Do not detach it with `launchAff_`; Halo can own and cancel only the returned computation. + +## Handle a UI action ADT Rendering code receives `dispatch :: action -> Effect Unit`. Each dispatch starts `handlers.onAction action` in the current component scope: @@ -57,12 +59,31 @@ handlers = Halo.defaultHandlers } ``` -Actions are concurrent by default, matching the event-driven model: one long-running action does not block later dispatches. Work is still cancelled when the React scope deactivates. Use a component-owned fork when another action needs an ID with which to cancel a process. +Action handlers overlap. A long-running action does not block a later dispatch, and React deactivation cancels every handler still running in that activation. + +When a process must outlive its handler or another action must cancel it, start a component-owned fork instead of leaving the work in the handler. -## Work with state and props +## Update state without stale snapshots `HaloM` has `MonadState state`. Use normal `get`, `put`, `gets`, `modify`, and `modify_` operations. +State operations run against the state current at that operation. Avoid reading a whole state value, waiting for an application effect, and then writing a modified copy of the old value: + +```purescript +-- Avoid: another action can update state while save runs. +old <- get +result <- lift (save old.form) +put (old { result = Just result }) +``` + +Capture only the input needed by the effect, then update the current state after it completes: + +```purescript +form <- gets _.form +result <- lift (save form) +modify_ _ { result = Just result } +``` + `Halo.getProps` reads the latest props. `onPropsChange` receives the previous props, so both sides of a synchronization are available: ```purescript @@ -71,9 +92,7 @@ onPropsChange = \previous -> do synchronize previous current ``` -State commits are fenced. After a handler or fork is killed, or after its activation deactivates, later Halo state operations can still compute their return value but cannot commit a new state or call React's state setter. - -Capture values associated with an action before starting work when they must not change during that work. `getProps` intentionally reads current props rather than a render snapshot. +Capture props before asynchronous work when that work must use one render's value. Otherwise, a later `getProps` intentionally returns newer props. ## Start and kill component processes @@ -92,15 +111,26 @@ startSearch query = do modify_ _ { searchFiber = Just fiber } ``` -`Halo.kill id` removes the fork from component tracking, fences its state and capabilities synchronously, requests Aff cancellation, and waits for cancellation and Aff finalizers before returning. Killing an unknown or completed ID does nothing. A fork inherits its launching root's application interpreter snapshot, even when a render supplies a newer interpreter before the fork starts. +`Halo.kill id` removes a tracked fork, fences it synchronously, requests Aff cancellation, and waits for cancellation and Aff finalizers before returning. Killing an unknown or completed ID does nothing. + +A killed or deactivated root cannot commit Halo state, register another Halo-owned capability, or start a newly lifted application effect—even if it catches the initial Aff cancellation. Cancellation cannot retract an HTTP request, storage write, callback, or log that already happened. Design external writes for retry and idempotency where needed. + +## Clean up at the correct boundary + +Halo intentionally has no asynchronous `onDeactivate` handler. React effect cleanup is synchronous, so React cannot wait for a `HaloM`, `AppM`, or `Aff` callback. Starting detached work during cleanup would also escape component ownership. -Deactivation cannot wait asynchronously because React cleanup is synchronous. It fences the whole activation first, attempts every subscription cleanup, and requests cancellation of all remaining handlers and forks. Aff finalizers continue in their cancellation fibers, but they cannot commit Halo state or start a newly lifted application effect after the fence. +Choose cleanup according to the resource: -Cancellation is cooperative. It cannot retract an HTTP request, storage write, callback, or log that already happened. Design external writes for retry and idempotency where needed. +- **Component process:** acquire and use the resource inside `fork` with an Aff finalizer. Deactivation requests cancellation of the fork. +- **Event source:** return synchronous cleanup from `makeEmitter`; Halo runs it while deactivating the subscription scope. +- **User cancellation:** retain the `ForkId` and call `kill`, which waits for finalizers. +- **Persistence:** save during normal application flow. Do not rely on unmount completing asynchronous persistence. + +Deactivation first fences the activation, then runs subscription cleanup and requests cancellation of handlers and forks. React cannot wait for those Aff cancellations, but their finalizers cannot commit Halo state or begin new lifted effects after the fence. ## Run independent work in parallel -`HaloM` has a direct `Parallel` instance with abstract counterpart `HaloAp`. Branches share the same root, scope, and interpreter snapshot: +`HaloM` has a direct `Parallel` instance with abstract counterpart `HaloAp`. Parallel branches share one root, component scope, and interpreter snapshot: ```purescript loadDashboard = do @@ -112,9 +142,9 @@ loadDashboard = do modify_ _ { profile = profile, feed = feed } ``` -Prefer parallel application reads followed by one Halo state commit. Concurrent Halo state writes have nondeterministic ordering; a later commit can overwrite an earlier one. +Prefer independent application reads followed by one Halo state update. Concurrent state writes have nondeterministic ordering and can overwrite one another. -Parallel work is lexical: the combined computation waits for its branches. Use `fork` only when work must continue independently of the launching handler or needs explicit cancellation by ID. +Parallel work is lexical: the combined computation waits for every branch. Use `fork` when work must continue independently of the launching handler or needs explicit cancellation by ID. ## Configure lifecycle handlers @@ -126,25 +156,23 @@ type Handlers props state action m = } ``` -Start with `defaultHandlers` and replace only what the component needs. +Start with `defaultHandlers` and replace only the callbacks the component needs. ### `onActivate` -Halo calls this for every React effect activation. Development StrictMode can run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable. Work from a prior activation is fenced and cancelled before a new activation becomes current. +Halo calls this for every React effect activation. Development StrictMode can run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable, not as an exactly-once mount event. ### `onPropsChange previousProps` -Halo starts this when the props reference changes. Read current props with `getProps`. New prop-change roots use the latest handlers and interpreter supplied by the hook. +Halo starts this when the props reference changes. Read current props with `getProps`. The root uses the latest handlers and interpreter supplied to the hook. ### `onAction action` -Halo starts one root for every action dispatched while active, including actions emitted by subscriptions. Dispatch while inactive is ignored. - -There is no asynchronous deactivation callback. Use subscriptions, Aff finalizers, or an external resource owner with explicit cleanup semantics. +Halo starts one root for each action dispatched while active, including actions emitted by subscriptions. Dispatch while inactive is ignored. -## Subscribe to custom emitters +## Subscribe to event sources -Halo's emitter avoids a Halogen dependency: +Halo's emitter API avoids a Halogen dependency: ```purescript events = Halo.makeEmitter \emit -> do @@ -152,21 +180,21 @@ events = Halo.makeEmitter \emit -> do pure (source.remove listener) ``` -`subscribe events` registers an action source in the current activation. `subscribeWithId (\id -> emitterFor id)` exposes the allocated `SubscriptionId` during registration. `unsubscribe id` removes tracking before running cleanup. +`subscribe events` registers an action source in the current activation. `subscribeWithId (\id -> emitterFor id)` also exposes the allocated `SubscriptionId`. `unsubscribe id` removes tracking before running cleanup. -Deactivation attempts every tracked cleanup even when one throws. Cleanup failures are reported as `DeactivationError` only after Halo has requested cleanup for the rest of the scope. A callback retained by a faulty source remains bound to its original activation and cannot dispatch into a later StrictMode reactivation. +Deactivation attempts every tracked cleanup even when one throws. Cleanup failures are reported as `DeactivationError` without preventing the remaining cleanup and cancellation requests. A callback retained by a faulty source stays bound to its original activation and cannot dispatch into a later StrictMode activation. Emitters broadcast actions. They do not provide backpressure, consuming queues, or scheduling policies. ## Handle unexpected errors -Every spec supplies: +Every component or hook spec supplies: ```purescript onError :: ErrorContext props action -> Error -> Effect Unit ``` -Contexts are: +Contexts identify the failed root or cleanup: - `ActivationError` for `onActivate`; - `PropsChangeError previousProps` for `onPropsChange`; @@ -174,10 +202,18 @@ Contexts are: - `ForkError id` for a component-owned fork; and - `DeactivationError` for throwing subscription cleanup. -Halo selects the latest `onError` callback when an unexpected failure is reported. Expected domain failures belong in application values or Halo state. Cancellation initiated by Halo is suppressed because the root has already been fenced. +Halo selects the latest `onError` callback when reporting a failure. Expected domain failures belong in application values, actions, or state. Halo-initiated cancellation is suppressed after its root is fenced. ## Choose `component` or `useHalo` -Use `Halo.component` when Halo owns the whole component. The renderer receives `{ props, state, dispatch }`. `initialState` receives initial props once per mount; synchronize later changes in `onPropsChange`. +Use `Halo.component` when Halo owns the complete component. Its renderer receives `{ props, state, dispatch }`. `initialState` receives initial props once per mount; synchronize later prop changes in `onPropsChange`. + +Use `Halo.useHalo` when other React hooks share the render function. It accepts the same application interpreter and returns `{ state, dispatch }`. + +## Common mistakes -Use `Halo.useHalo` when other React hooks share the render function. It accepts the same interpreter and returns `{ state, dispatch }`. +- **An activation runs twice in development:** React StrictMode replayed setup. Make `onActivate` repeatable. +- **A long-running action cannot be cancelled by another action:** move that work into `fork` and retain its `ForkId`. +- **A state update overwrites newer input:** capture only effect inputs before waiting, then update current state with `modify_`. +- **Cleanup needs asynchronous work:** use an Aff finalizer in a component-owned fork; React cannot await an asynchronous deactivation callback. +- **An old event callback still fires:** Halo rejects its dispatch if the activation is stale, but the external source must still implement cleanup correctly. diff --git a/docs/reference.md b/docs/reference.md deleted file mode 100644 index 90f1f46..0000000 --- a/docs/reference.md +++ /dev/null @@ -1,195 +0,0 @@ -# Halo v4 API reference - -Import the intentional public surface from `React.Halo`: - -```purescript -import React.Halo as Halo -``` - -Runtime constructors and ownership records are internal. `ForkId` and `SubscriptionId` constructors are hidden. - -## Core computation - -```purescript -HaloM props state action m a -HaloAp props state action m a -``` - -`HaloM` is the sequential component computation. Its parameters are current props, Halo state, dispatched actions, the application's base monad, and the result. - -It has unconditional `Functor`, `Apply`, `Applicative`, `Bind`, `Monad`, and `MonadState state` instances. It is a `MonadTrans` in its `m` parameter: - -```purescript -lift :: Monad m => m a -> HaloM props state action m a -``` - -The following capabilities are lifted through `m`, rather than executed directly in Halo's private `Aff`: - -```purescript -MonadEffect m => MonadEffect (HaloM props state action m) -MonadAff m => MonadAff (HaloM props state action m) -MonadAsk environment m => MonadAsk environment (HaloM props state action m) -MonadTell output m => MonadTell output (HaloM props state action m) -MonadThrow error m => MonadThrow error (HaloM props state action m) -``` - -Each `lift` checks the root fence before invoking the captured interpreter. A stale root therefore cannot start a new application effect, even when it catches its initial Aff cancellation. - -`HaloAp` is the abstract parallel applicative counterpart: - -```purescript -Parallel - (HaloAp props state action m) - (HaloM props state action m) -``` - -Parallel branches share their root, component scope, and `m ~> Aff` interpreter snapshot. Concurrent Halo state writes have nondeterministic ordering; combine independent results before committing state when possible. - -## Handlers - -```purescript -type Handlers props state action m = - { onActivate :: HaloM props state action m Unit - , onPropsChange :: props -> HaloM props state action m Unit - , onAction :: action -> HaloM props state action m Unit - } - -defaultHandlers - :: forall props state action m - . Handlers props state action m -``` - -Handlers are component-activation-owned roots: - -- `onActivate` runs for each React effect activation and is repeatable under StrictMode. -- `onPropsChange previousProps` starts when the props reference changes. -- `onAction action` starts for each dispatch while active. - -Handlers selected for new roots come from the latest hook spec. There is no asynchronous deactivation handler. - -## State and props - -Use the `MonadState state` operations for Halo state. - -```purescript -getProps - :: forall props state action m - . HaloM props state action m props -``` - -`getProps` returns the latest props. State commits from a stale root are ignored. - -## Component-owned forks - -```purescript -fork - :: forall props state action m - . HaloM props state action m Unit - -> HaloM props state action m ForkId - -kill - :: forall props state action m - . ForkId - -> HaloM props state action m Unit -``` - -`fork` starts a root owned by the current React activation. It may outlive its launching handler. The child uses the launching root's interpreter snapshot, but receives an independent state/capability fence and reports unexpected failures as `ForkError id`. - -`kill` removes a tracked fork, fences its Halo state and capabilities synchronously, and then waits for Aff cancellation and finalizers. Killing an unknown or completed ID does nothing. Deactivation fences and requests cancellation of every remaining fork and handler. - -## Subscriptions and emitters - -```purescript -makeEmitter - :: forall action - . ((action -> Effect Unit) -> Effect (Effect Unit)) - -> Emitter action - -subscribe - :: forall props state action m - . Emitter action - -> HaloM props state action m SubscriptionId - -subscribeWithId - :: forall props state action m - . (SubscriptionId -> Emitter action) - -> HaloM props state action m SubscriptionId - -unsubscribe - :: forall props state action m - . SubscriptionId - -> HaloM props state action m Unit -``` - -Emitter registration receives a receiver and returns its cleanup effect. Emissions dispatch into the activation that registered the receiver. A stale callback cannot target a later activation. - -Manual unsubscription removes tracking before cleanup runs. Deactivation attempts every remaining cleanup; throwing cleanup is isolated and reported as `DeactivationError` without preventing other cleanup and cancellation requests. - -## Errors - -```purescript -data ErrorContext props action - = ActivationError - | PropsChangeError props - | ActionError action - | ForkError ForkId - | DeactivationError - -onError - :: ErrorContext props action - -> Error - -> Effect Unit -``` - -Halo reports unexpected root failures through the latest `onError` callback. Cancellation initiated by Halo is suppressed. Expected domain failures should be represented in application values, actions, or state. - -## Hook API - -```purescript -type HookSpec props state action m = - { handlers :: Handlers props state action m - , initialState :: state - , onError :: ErrorContext props action -> Error -> Effect Unit - , props :: props - } - -type HaloResult state action = - { dispatch :: action -> Effect Unit - , state :: state - } - -useHalo - :: forall props state action m - . (m ~> Aff) - -> HookSpec props state action m - -> Hook - (UseHalo props state action m) - (HaloResult state action) -``` - -Each new handler captures the latest natural transformation supplied to the hook. A fork inherits the transformation captured by the root that launches it, even if a newer render supplied another interpreter before the fork starts. Cleanup deactivates the current scope; StrictMode reactivation creates a fresh usable scope. - -## Component API - -```purescript -type ComponentSpec props state action m = - { handlers :: Handlers props state action m - , initialState :: props -> state - , onError :: ErrorContext props action -> Error -> Effect Unit - , render :: - { dispatch :: action -> Effect Unit - , props :: props - , state :: state - } - -> JSX - } - -component - :: forall props state action m - . String - -> (m ~> Aff) - -> ComponentSpec props state action m - -> Component props -``` - -`initialState` receives initial props once per mount. Later prop changes invoke `handlers.onPropsChange` and do not reinitialize state. Use `component` when Halo owns the whole component and `useHalo` when other hooks share the render function. diff --git a/src/React/Halo.purs b/src/React/Halo.purs index 62b2c28..e3ad67b 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -1,3 +1,12 @@ +-- | Component-scoped action handling for PureScript React. +-- | +-- | A Halo component keeps application effects in a caller-defined monad `m`. +-- | `component` or `useHalo` receives a natural transformation from `m` to +-- | `Aff`, while `HaloM` adds component props, state, actions, cancellable +-- | forks, subscriptions, and lifecycle ownership. +-- | +-- | Import this module for the intentional public API. Runtime constructors and +-- | ownership records remain internal. module React.Halo ( module Exports ) where diff --git a/src/React/Halo/Component.purs b/src/React/Halo/Component.purs index 9cfbd0d..6011c88 100644 --- a/src/React/Halo/Component.purs +++ b/src/React/Halo/Component.purs @@ -13,10 +13,11 @@ import React.Halo.Handlers (Handlers) import React.Halo.Hook (useHalo) import React.Halo.Internal.Types (ErrorContext) --- | Complete configuration for a Halo-owned React component. +-- | Configuration for a complete Halo-owned React component. -- | -- | `initialState` receives the initial props once per mount. Later prop changes --- | run `handlers.onPropsChange` and do not recreate state. +-- | start `handlers.onPropsChange` and do not recreate state. The renderer +-- | receives current props and state plus synchronous action dispatch. type ComponentSpec props state action m = { handlers :: Handlers props state action m , initialState :: props -> state @@ -31,8 +32,9 @@ type ComponentSpec props state action m = -- | Build a complete React component around a Halo action runtime. -- | --- | The natural transformation is the application boundary: it translates the --- | component's application monad into the `Aff` fibers owned by Halo. +-- | The natural transformation is the application boundary: it translates `m` +-- | into the component-owned `Aff` roots managed by Halo. It must return the +-- | computation that performs the work rather than detach it. component :: forall props state action m . String diff --git a/src/React/Halo/Handlers.purs b/src/React/Halo/Handlers.purs index 4ea6913..d7a207f 100644 --- a/src/React/Halo/Handlers.purs +++ b/src/React/Halo/Handlers.purs @@ -9,10 +9,15 @@ import React.Halo.Internal.Runtime (Handlers) as Runtime -- | Activation, prop-change, and action callbacks for a Halo component. -- | --- | `onActivate` may run again after React replays an effect setup. It is not an --- | exactly-once mount callback. `onPropsChange` receives the previous props; --- | use `React.Halo.getProps` to read the current props. `onAction` starts for --- | every dispatched action. +-- | Each invocation is an independent root owned by the current React +-- | activation. `onActivate` is repeatable under StrictMode, not an exactly-once +-- | mount callback. `onPropsChange` receives the previous props; use +-- | `React.Halo.getProps` to read current props. `onAction` starts for every +-- | dispatched action, so action handlers can overlap. +-- | +-- | There is no asynchronous deactivation callback because React cleanup is +-- | synchronous. Use Aff finalizers for forked processes and emitter cleanup +-- | for subscriptions. type Handlers props state action m = Runtime.Handlers props state action m -- | Handlers that do nothing. Use a record update to configure only the diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index 468d675..423a4b6 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -19,6 +19,11 @@ import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate import React.Halo.Internal.Types (ErrorContext) -- | Configuration for `useHalo`. +-- | +-- | `initialState` initializes the hook once. Later `props` changes start +-- | `handlers.onPropsChange` without recreating state. New roots use the latest +-- | handlers, error callback, state setter, and application interpreter supplied +-- | by a render. type HookSpec props state action m = { handlers :: Handlers props state action m , initialState :: state @@ -26,7 +31,9 @@ type HookSpec props state action m = , props :: props } --- | State and action dispatch exposed to rendering code. +-- | Current component state and synchronous action dispatch exposed to +-- | rendering code. Dispatch starts an independent handler root while the +-- | current React activation is active. type HaloResult state action = { dispatch :: action -> Effect Unit , state :: state @@ -47,10 +54,15 @@ derive instance newtypeUseHalo :: Newtype (UseHalo props state action m hooks) _ -- | Run Halo inside a `react-basic-hooks` component. -- | --- | The natural transformation interprets application effects in `m` into the --- | `Aff` fibers owned by the active React scope. New handlers use the latest --- | interpreter; roots already running retain their starting snapshot, and a --- | fork inherits the snapshot of the root that launches it. +-- | The natural transformation interprets application effects in `m` inside +-- | `Aff` roots owned by the active React scope. It must return the computation +-- | that performs the work rather than detach it. New handlers use the latest +-- | interpreter; existing roots retain their snapshot, and a fork inherits the +-- | snapshot of the root that launches it. +-- | +-- | Effect cleanup fences the activation, runs subscription cleanup, and +-- | requests cancellation of every handler and fork. A StrictMode setup replay +-- | creates a fresh usable activation. useHalo :: forall props state action m . (m ~> Aff) diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index d9fed6d..ee2de50 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -47,16 +47,23 @@ import React.Halo.Subscription (Emitter) import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) --- | The Halo component computation. Application effects in `m` are translated --- | into the current root's `Aff` fiber by the interpreter supplied to --- | `component` or `useHalo`. +-- | A sequential component computation over an application monad `m`. +-- | +-- | Standard `lift` runs an `m` value through the interpreter supplied to +-- | `component` or `useHalo`. Every lift checks the current ownership fence, so +-- | killed or deactivated work cannot begin another application effect. HaloM +-- | also provides component `MonadState`; its other lifted capabilities, such +-- | as `MonadEffect`, `MonadAff`, `MonadAsk`, `MonadTell`, and `MonadThrow`, pass +-- | through `m`. newtype HaloM props state action (m :: Type -> Type) a = HaloM (ReaderT (Execution props state action m) Aff a) --- | The parallel applicative counterpart of `HaloM`. +-- | The abstract parallel applicative counterpart of `HaloM`. -- | --- | Parallel branches share the current root, component scope, and application --- | interpreter snapshot. +-- | Parallel branches share one root, component scope, and application +-- | interpreter snapshot. Concurrent component-state writes have +-- | nondeterministic ordering; prefer combining independent application results +-- | before one state update. newtype HaloAp props state action (m :: Type -> Type) a = HaloAp (ReaderT (Execution props state action m) ParAff a) @@ -113,8 +120,12 @@ instance monadStateHaloM :: MonadState state (HaloM props state action m) where pure result else pure result --- | Activation, prop-change, and action callbacks. Each callback is a --- | component-scope root and may perform application effects directly. +-- | Activation, prop-change, and action callbacks. +-- | +-- | Every invocation starts an independent root in the current React +-- | activation. Handlers can overlap and are cancelled when that activation +-- | deactivates. `onPropsChange` receives the previous props; `getProps` reads +-- | the current props. type Handlers props state action m = { onActivate :: HaloM props state action m Unit , onPropsChange :: props -> HaloM props state action m Unit @@ -292,15 +303,19 @@ dispatchToScope runtime@(Runtime state) scope action = do spec <- Ref.read state.spec startHandler runtime scope (ActionError action) (spec.handlers.onAction action) --- | Read the latest component props. +-- | Read the latest component props, rather than the props captured when the +-- | current root started. getProps :: forall props state action m. HaloM props state action m props getProps = HaloM $ ReaderT \execution -> do let Runtime runtime = execution.runtime liftEffect $ Ref.read runtime.props --- | Start work owned by the current React activation. The fork may outlive its --- | launching handler, inherits that root's interpreter snapshot, and is --- | cancelled on explicit `kill` or deactivation. +-- | Start a process owned by the current React activation. +-- | +-- | The fork may outlive its launching handler and has an independent +-- | cancellation fence, but it inherits that root's application-interpreter +-- | snapshot. It is cancelled by `kill` or activation deactivation. Unexpected +-- | failure is reported as `ForkError`. fork :: forall props state action m . HaloM props state action m Unit @@ -318,8 +333,12 @@ fork child = HaloM $ ReaderT \execution -> do prepared.start pure fid --- | Cancel a component-owned fork. Halo fences the fork synchronously, then --- | waits for its Aff cancellation and finalizers before returning. +-- | Cancel a component-owned fork. +-- | +-- | Halo removes and fences the fork synchronously, then waits for Aff +-- | cancellation and finalizers. The fence blocks later state commits, Halo +-- | capabilities, and lifted application effects. An unknown or completed ID +-- | is ignored. kill :: forall props state action m . ForkId @@ -342,15 +361,19 @@ kill fid = HaloM $ ReaderT \execution -> do ) root --- | Register an emitter in the current activation scope. Its cleanup runs on --- | manual unsubscription or deactivation. +-- | Register an action emitter in the current activation scope. +-- | +-- | Its synchronous cleanup runs on manual unsubscription or deactivation. +-- | Emissions stay bound to the activation that registered them, so a retained +-- | stale callback cannot dispatch into a later activation. subscribe :: forall props state action m . Emitter action -> HaloM props state action m SubscriptionId subscribe = subscribeWithId <<< const --- | Subscribe while providing the allocated identifier to the emitter. +-- | Subscribe while providing the allocated identifier to the emitter's +-- | registration logic. subscribeWithId :: forall props state action m . (SubscriptionId -> Emitter action) @@ -366,8 +389,10 @@ subscribeWithId makeEmitter = HaloM $ ReaderT \execution -> do Ref.modify_ (Map.insert sid cleanup) scope.subscriptions pure sid --- | Remove a tracked subscription before running its cleanup. A throwing --- | cleanup therefore cannot be retried during deactivation. +-- | Remove a tracked subscription before running its cleanup. +-- | +-- | A throwing cleanup therefore cannot be retried during deactivation; when +-- | called from a root, the failure is routed through that root's error context. unsubscribe :: forall props state action m . SubscriptionId diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index a87129c..459d7e7 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -6,8 +6,12 @@ module React.Halo.Internal.Types import Prelude --- | Identifies the component-owned computation whose unexpected failure reached --- | `onError`. +-- | Identifies the component-owned computation or cleanup whose unexpected +-- | failure reached `onError`. +-- | +-- | `PropsChangeError` carries the previous props, `ActionError` carries the +-- | dispatched action, and `ForkError` carries the component-owned fork ID. +-- | Halo-initiated cancellation is fenced and is not reported. data ErrorContext props action = ActivationError | PropsChangeError props @@ -15,7 +19,8 @@ data ErrorContext props action | ForkError ForkId | DeactivationError --- | Identifies a component-owned fiber created with `fork`. +-- | Identifies a component-owned process created with `fork`. Its constructor +-- | is hidden from the root `React.Halo` API. newtype ForkId = ForkId Int derive newtype instance eqForkId :: Eq ForkId @@ -24,7 +29,8 @@ derive newtype instance ordForkId :: Ord ForkId derive newtype instance showForkId :: Show ForkId --- | Identifies a component-scoped emitter subscription. +-- | Identifies an emitter subscription in one React activation. Its constructor +-- | is hidden from the root `React.Halo` API. newtype SubscriptionId = SubscriptionId Int derive newtype instance eqSubscriptionId :: Eq SubscriptionId diff --git a/src/React/Halo/Subscription.purs b/src/React/Halo/Subscription.purs index feb24b5..f812589 100644 --- a/src/React/Halo/Subscription.purs +++ b/src/React/Halo/Subscription.purs @@ -10,15 +10,17 @@ import Effect (Effect) -- | A source that broadcasts actions to each registered receiver. -- | --- | Registration returns the cleanup effect for that receiver. Halo runs the --- | cleanup when the subscription is removed or its activation scope ends. --- | Emitters broadcast and do not provide consuming-queue or backpressure --- | semantics. +-- | Registration returns synchronous cleanup for that receiver. Halo runs it +-- | when the subscription is removed or its React activation ends. A receiver +-- | remains bound to the activation that registered it, so a stale callback +-- | cannot dispatch into a later activation. Emitters do not provide a +-- | consuming queue or backpressure. newtype Emitter action = Emitter ((action -> Effect Unit) -> Effect (Effect Unit)) --- | Create an emitter from registration logic. A throwing cleanup is isolated --- | from other scope cleanup and reported as `DeactivationError`. +-- | Create an emitter from registration logic. During deactivation, a throwing +-- | cleanup is isolated from the remaining scope cleanup and reported as +-- | `DeactivationError`. makeEmitter :: forall action . ((action -> Effect Unit) -> Effect (Effect Unit)) diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index 7e8748d..4b8d0f7 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -5,12 +5,9 @@ import Prelude import Control.Monad.Reader (ReaderT, ask, runReaderT) import Control.Monad.State (gets, modify_) import Control.Monad.Trans.Class (lift) -import Control.Parallel (parallel, sequential) -import Data.Either (Either(..)) import Data.Foldable (traverse_) import Data.Maybe (Maybe(..)) -import Data.Tuple (Tuple(..)) -import Effect.Aff (Aff, attempt) +import Effect.Aff (Aff) import Effect.Aff.Class (class MonadAff, liftAff) import Effect.Class (class MonadEffect) import Effect.Class.Console as Console @@ -35,20 +32,17 @@ derive newtype instance monadAffAppM :: MonadAff AppM runAppM :: Env -> AppM ~> Aff runAppM env (AppM computation) = runReaderT computation env -loadGreeting :: AppM (Either String String) +loadGreeting :: AppM String loadGreeting = AppM do env <- ask - outcome <- liftAff $ attempt env.loadGreeting - pure case outcome of - Left error -> Left (message error) - Right greeting -> Right greeting + liftAff env.loadGreeting type Props = { title :: String } type State = { fiber :: Maybe Halo.ForkId , loading :: Boolean - , result :: Maybe (Either String String) + , result :: Maybe String } data Action @@ -78,8 +72,8 @@ loadButton :: Env -> Component Props loadButton env = Halo.component "LoadButton" (runAppM env) { initialState: \_ -> { fiber: Nothing, loading: false, result: Nothing } , handlers - , onError: \context error -> - Console.error $ "Unexpected Halo failure in " <> showContext context <> ": " <> message error + , onError: \_ error -> + Console.error $ "Unexpected Halo error: " <> message error , render: \{ props, state, dispatch } -> R.div_ [ R.text props.title @@ -93,8 +87,7 @@ loadButton env = Halo.component "LoadButton" (runAppM env) } , R.text $ case state.result of Nothing -> if state.loading then "Loading…" else "Not loaded" - Just (Left error) -> error - Just (Right greeting) -> greeting + Just greeting -> greeting ] } @@ -108,28 +101,3 @@ useExample env props = Halo.useHalo (runAppM env) , handlers , onError: \_ _ -> pure unit } - -parallelExample :: UI Unit -parallelExample = do - Tuple a b <- sequential ado - a <- parallel $ lift (pure 1 :: AppM Int) - b <- parallel $ lift (pure 2 :: AppM Int) - in Tuple a b - modify_ _ { loading = a + b < 0 } - -showContext :: Halo.ErrorContext Props Action -> String -showContext = case _ of - Halo.ActivationError -> "activation" - Halo.PropsChangeError _ -> "props change" - Halo.ActionError Load -> "Load action" - Halo.ActionError Cancel -> "Cancel action" - Halo.ForkError _ -> "fork" - Halo.DeactivationError -> "deactivation" - -data SimpleAction = InitializeData - -simpleEmitter :: Halo.Emitter SimpleAction -simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) - -simpleSubscription :: Halo.HaloM Unit Unit SimpleAction AppM Unit -simpleSubscription = void $ Halo.subscribeWithId \_ -> simpleEmitter diff --git a/test/Test/Halo/GuideExamples.purs b/test/Test/Halo/GuideExamples.purs new file mode 100644 index 0000000..b265e63 --- /dev/null +++ b/test/Test/Halo/GuideExamples.purs @@ -0,0 +1,32 @@ +module Test.Halo.GuideExamples where + +import Prelude + +import Control.Monad.State (modify_) +import Control.Monad.Trans.Class (lift) +import Control.Parallel (parallel, sequential) +import Data.Tuple (Tuple(..)) +import Effect.Aff (Aff) +import React.Halo as Halo + +type DashboardState = + { feed :: Int + , profile :: Int + } + +parallelExample :: Halo.HaloM Unit DashboardState Unit Aff Unit +parallelExample = do + Tuple profile feed <- sequential ado + profile <- parallel $ lift (pure 1 :: Aff Int) + feed <- parallel $ lift (pure 2 :: Aff Int) + in Tuple profile feed + + modify_ _ { profile = profile, feed = feed } + +data SimpleAction = InitializeData + +simpleEmitter :: Halo.Emitter SimpleAction +simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) + +simpleSubscription :: Halo.HaloM Unit Unit SimpleAction Aff Unit +simpleSubscription = void $ Halo.subscribeWithId \_ -> simpleEmitter From dcc9dffc065bbfef37194ded6cd36d3fa5b3ee11 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Wed, 2 Sep 2026 00:55:42 +0900 Subject: [PATCH 11/16] Add state-focused tasks and scoped cleanup --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 56 ++- docs/architecture.md | 25 +- docs/guide.md | 63 ++- spago.lock | 29 ++ spago.yaml | 1 + src/React/Halo.purs | 6 +- src/React/Halo/Internal/Runtime.purs | 241 +++++++++- src/React/Halo/Internal/Task.purs | 304 +++++++++++++ src/React/Halo/Internal/Types.purs | 13 +- src/React/Halo/Task.purs | 33 ++ test/Main.purs | 2 + test/Test/Halo/DocExamples.purs | 45 +- test/Test/Halo/GuideExamples.purs | 41 +- test/Test/Halo/SubscriptionErrorSpec.purs | 183 +++++++- test/Test/Halo/TaskSpec.purs | 526 ++++++++++++++++++++++ 17 files changed, 1483 insertions(+), 89 deletions(-) create mode 100644 src/React/Halo/Internal/Task.purs create mode 100644 src/React/Halo/Task.purs create mode 100644 test/Test/Halo/TaskSpec.purs diff --git a/AGENTS.md b/AGENTS.md index f753a11..1dace2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Before changing dependencies or developer commands, inspect `package.json`, `spa ## Preserve repository boundaries -- Keep `React.Halo` as the intentional public API root. Do not expose internal runtime ownership types as a shortcut. +- Keep `React.Halo` as the intentional public API root. Keep the state-focused API in the separately qualified `React.Halo.Task`; do not flatten its generic names through the root. Do not expose internal runtime ownership types as a shortcut. - Keep React integration in the component/hook boundary and runtime ownership in `React.Halo.Internal.Runtime`. Read the architecture document instead of duplicating its rules here. - Keep application capabilities routed through the application monad and its `m ~> Aff` interpreter. - Do not add npm runtime dependencies or an npm runtime entry point. The npm package is development tooling only. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d192a5b..0b8a285 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ The documentation command writes generated API pages to `generated-docs/`. Spago ## Keep behavior, tests, and docs aligned -`React.Halo` is the public API root. When a change affects its exported types or behavior: +`React.Halo` is the public API root; the generic state-focused names in `React.Halo.Task` form a separate qualified surface. When a change affects either module's exported types or behavior: - update the relevant public module documentation; - update the README or guide when their guidance changes, and keep exact API contracts in public source comments used by generated documentation; diff --git a/README.md b/README.md index 2baa94a..b0a56e2 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,12 @@ package: - aff - console - effect + - either - exceptions - foldable-traversable - maybe - prelude + - profunctor-lenses - react-basic-dom - react-basic-hooks - react-halo @@ -44,7 +46,7 @@ workspace: After v4 is published, the local override can be replaced with: ```console -spago install aff console effect exceptions foldable-traversable maybe prelude react-basic-dom react-basic-hooks react-halo transformers +spago install aff console effect either exceptions foldable-traversable maybe prelude profunctor-lenses react-basic-dom react-basic-hooks react-halo transformers ``` `react-basic-dom` is used by this example, not required by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. @@ -75,17 +77,23 @@ loadGreeting = AppM do liftAff env.loadGreeting ``` -Define component state and an action ADT. Store a `ForkId` when another action must be able to cancel the process: +Define component state and an action ADT. Import `React.Halo.Task` qualified and locate its abstract state with a standard lens: ```purescript +import Data.Lens (Lens') +import Data.Lens.Record (prop) +import React.Halo.Task as Task +import Type.Proxy (Proxy(..)) + type Props = { title :: String } type State = - { fiber :: Maybe Halo.ForkId - , loading :: Boolean - , result :: Maybe String + { greeting :: Task.State String String } +greetingLens :: Lens' State (Task.State String String) +greetingLens = prop (Proxy :: Proxy "greeting") + data Action = Load | Cancel type UI a = Halo.HaloM Props State Action AppM a @@ -93,23 +101,17 @@ type UI a = Halo.HaloM Props State Action AppM a handlers :: Halo.Handlers Props State Action AppM handlers = Halo.defaultHandlers { onAction = case _ of - Load -> do - previous <- gets _.fiber - traverse_ Halo.kill previous - fiber <- Halo.fork do - modify_ _ { loading = true, result = Nothing } - result <- lift loadGreeting - modify_ _ { loading = false, result = Just result } - modify_ _ { fiber = Just fiber } - - Cancel -> do - previous <- gets _.fiber - traverse_ Halo.kill previous - modify_ _ { fiber = Nothing, loading = false } + Load -> Task.supersede greetingLens do + greeting <- lift loadGreeting + pure (Right greeting) + + Cancel -> Task.reset greetingLens } ``` -`lift` is `Control.Monad.Trans.Class.lift`. Each handler captures the interpreter current when it starts. A fork inherits its launching handler's interpreter, even if React renders with a newer interpreter before the fork begins. +A task body is ordinary `HaloM` and returns `Either error result`. `supersede` makes the new invocation authoritative immediately; `reset` cancels active work and waits for its Aff finalizers. Rendering sees only `Idle`, `Active`, `Failed error`, or `Succeeded result`; hidden run identity prevents stale completion from overwriting newer state. + +`lift` is `Control.Monad.Trans.Class.lift`. Each handler captures the interpreter current when it starts. Managed tasks and forks inherit their launching handler's interpreter, even if React renders with a newer interpreter before their bodies begin. Supply the interpreter when creating the component: @@ -117,7 +119,7 @@ Supply the interpreter when creating the component: loadButton :: Env -> Component Props loadButton env = Halo.component "LoadButton" (runAppM env) { initialState: \_ -> - { fiber: Nothing, loading: false, result: Nothing } + { greeting: Task.idle } , handlers , onError: \_ error -> Console.error $ "Unexpected Halo error: " <> message error @@ -126,15 +128,17 @@ loadButton env = Halo.component "LoadButton" (runAppM env) [ R.text props.title , R.button { onClick: capture_ (dispatch Load) - , children: [ R.text if state.loading then "Restart" else "Load" ] + , children: [ R.text if Task.isActive state.greeting then "Restart" else "Load" ] } , R.button { onClick: capture_ (dispatch Cancel) , children: [ R.text "Cancel" ] } - , R.text $ case state.result of - Nothing -> if state.loading then "Loading…" else "Not loaded" - Just greeting -> greeting + , R.text $ case Task.toStatus state.greeting of + Task.Idle -> "Not loaded" + Task.Active -> "Loading…" + Task.Failed error -> error + Task.Succeeded greeting -> greeting ] } ``` @@ -157,9 +161,11 @@ halo <- Halo.useHalo (runAppM env) A complete version of this example is compiled as [`test/Test/Halo/DocExamples.purs`](test/Test/Halo/DocExamples.purs). +For a synchronous resource that is not an emitter subscription, use `Halo.registerCleanup cleanup`. Halo runs every still-registered `Effect Unit` when the React activation deactivates. `Halo.releaseCleanup id` removes and runs one cleanup immediately; it is not an asynchronous deactivation callback. + ## Learn more -- The [Halo guide](docs/guide.md) explains actions, state, component processes, cancellation, parallelism, subscriptions, and errors. +- The [Halo guide](docs/guide.md) explains tasks, component processes, cleanup, cancellation, parallelism, subscriptions, and errors. - Generate the exact API reference from public source comments with `npx spago docs --offline`. - The [runtime architecture](docs/architecture.md) describes ownership and cancellation invariants for maintainers. - See [Contributing](CONTRIBUTING.md) before changing the library. diff --git a/docs/architecture.md b/docs/architecture.md index afcf848..4413b2f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,7 +28,7 @@ The hook returns only current state and `dispatch`. [`React.Halo.Internal.Runtim ## Each activation has a generation -An active runtime holds one scope with a unique generation, an active flag, and maps for handler roots, component forks, and subscriptions. Activation is idempotent while that scope is current. +An active runtime holds one scope with a unique generation, an active flag, and maps for handler roots, component forks, generic cleanup, and subscriptions. Activation is idempotent while that scope is current. Deactivation marks the scope inactive and clears it from the runtime before foreign cleanup or Aff cancellation begins. A later activation creates fresh maps and a new generation. Currency checks require both an active matching generation and a live root owner, so work retained from an earlier generation cannot affect a reactivated component. @@ -42,13 +42,19 @@ Every `onActivate`, `onPropsChange`, and `onAction` invocation starts an indepen Root completion removes only that root's current map entry. IDs are fresh within the runtime, so stale completion cannot remove newer work. +Managed task policies use the same fork map and root ownership rather than a scheduler or key registry. In one synchronous transaction the runtime claims lensed task state, fences a superseded root, prepares and records the replacement behind a gate, publishes `Active`, requests old cancellation, and opens the new gate. The task body inherits the launching root's interpreter. Hidden run identity makes delayed timers and completions conditional on the authoritative run. + +`Task.State` projects only `Idle`, `Active`, `Failed`, or `Succeeded`. Debounce timing remains private: an owned Aff timer precedes the body in the same managed root. A typed `Either` completion updates the lensed state atomically. An unexpected current failure clears the matching run to `Idle` before existing `ForkError` routing. + ## Fences precede cancellation Cancellation is cooperative, but ownership loss is synchronous. For explicit `kill`, the runtime removes the fork from tracking, fences its owner, requests Aff cancellation, and waits for the fiber and its Aff finalizers before returning. An unknown or completed `ForkId` is a no-op. -React deactivation cannot wait asynchronously. It invalidates the scope, takes all tracked roots and subscriptions, fences every root, attempts every synchronous subscription cleanup, and then requests cancellation of all handler and fork fibers. Cleanup failures are reported only after the runtime has attempted the rest of the cleanup work. +React deactivation cannot wait asynchronously. It invalidates the scope; takes all tracked roots, generic cleanup, and subscriptions; and fences every root. Managed active task state is then normalized to `Idle` in the runtime state without calling React's setter during cleanup. Every synchronous cleanup is attempted before cancellation is requested for handler, fork, and task fibers. Cleanup failures are reported only after the runtime has attempted the rest of the cleanup work. + +If deactivation normalized task state, the next activation publishes that runtime state through the latest React setter before starting `onActivate`. Terminal task outcomes have no active root and persist. This ordering keeps StrictMode replay state coherent without adding an asynchronous deactivation callback. The fences protect two important boundaries: @@ -57,6 +63,12 @@ The fences protect two important boundaries: Capabilities that create or remove forks and subscriptions also check currency. Cancellation cannot undo an external effect that already happened inside an application computation; application writes must still use appropriate idempotency or retry semantics. +## Synchronous cleanup stays activation-scoped + +Generic cleanup and emitter subscription cleanup use separate activation maps. `registerCleanup` stores an `Effect Unit`; `releaseCleanup` removes it before invocation. Deactivation takes both maps and attempts each disposer independently, so one throw cannot block another disposer or root cancellation. Deactivation failures share `DeactivationError`, but cross-category cleanup ordering is not a public contract. + +Cleanup IDs are runtime-fresh and a release consults only the current scope. An ID or stale root retained from one StrictMode activation therefore cannot remove resources from another. + ## Subscriptions close over their activation The local [`Emitter`](../src/React/Halo/Subscription.purs) registers an `Effect` callback and returns a synchronous cleanup. The scope tracks that cleanup by `SubscriptionId`. Manual unsubscription removes the entry before running cleanup, which prevents a throwing cleanup from being retried during deactivation. @@ -71,7 +83,7 @@ Concurrent Halo state writes have nondeterministic ordering and can overwrite on ## Errors use the current reporting callback -Each root carries the context assigned at launch: `ActivationError`, `PropsChangeError previousProps`, `ActionError action`, or `ForkError id`. An unexpected failure is reported only while that root and scope are still current. The runtime reads the latest `onError` callback at reporting time, so a render can update reporting without changing an already-running root's interpreter. +Each root carries the context assigned at launch: `ActivationError`, `PropsChangeError previousProps`, `ActionError action`, or `ForkError id`. Managed task bodies use `ForkError` because they are component-owned roots without public task keys. An unexpected failure is reported only while that root and scope are still current. The runtime reads the latest `onError` callback at reporting time, so a render can update reporting without changing an already-running root's interpreter. Subscription cleanup failures use `DeactivationError`. Halo-initiated cancellation and stale-lift failures are suppressed because the owner has already been fenced. @@ -81,8 +93,9 @@ The deterministic runtime tests exercise ownership without mounting a real DOM f - [`RuntimeSpec`](../test/Test/Halo/RuntimeSpec.purs) covers AppM interpretation, handler and fork interpreter snapshots, the stale-lift fence, and direct parallel execution. - [`ScopeHandlerSpec`](../test/Test/Halo/ScopeHandlerSpec.purs) covers activation generations, StrictMode reactivation, props, handler and fork ownership, explicit kill, finalizer waiting, and stale capability/state rejection. -- [`SubscriptionErrorSpec`](../test/Test/Halo/SubscriptionErrorSpec.purs) covers cleanup isolation, stale emitter callbacks, error contexts, and latest error-handler selection. -- [`DocExamples`](../test/Test/Halo/DocExamples.purs) compile-checks the complete component, hook, AppM, and fork example. -- [`GuideExamples`](../test/Test/Halo/GuideExamples.purs) compile-checks the guide's parallel and subscription examples. +- [`SubscriptionErrorSpec`](../test/Test/Halo/SubscriptionErrorSpec.purs) covers generic and subscription cleanup isolation, manual release, stale activation IDs and emitter callbacks, error contexts, and latest error-handler selection. +- [`TaskSpec`](../test/Test/Halo/TaskSpec.purs) covers task optics and policies, atomic claims, supersession and reset, private debounce scheduling, stale effect fences, deactivation normalization, current setters, and inherited interpreters. +- [`DocExamples`](../test/Test/Halo/DocExamples.purs) compile-checks the complete component, hook, AppM, and task example. +- [`GuideExamples`](../test/Test/Halo/GuideExamples.purs) compile-checks the guide's task, parallel, subscription, and cleanup examples. [`test/Main.purs`](../test/Main.purs) runs the full behavioral suite. Preserve these deterministic boundaries when changing the runtime; add a focused regression beside the invariant it protects. diff --git a/docs/guide.md b/docs/guide.md index 0bbc2d3..3257459 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -94,6 +94,64 @@ onPropsChange = \previous -> do Capture props before asynchronous work when that work must use one render's value. Otherwise, a later `getProps` intentionally returns newer props. +## Store typed task outcomes in component state + +Import `React.Halo.Task` qualified when component state should retain the lifecycle and typed result of owned work. `Task.State error result` is abstract because it includes hidden cancellation identity. Locate it with a standard lens: + +```purescript +import Data.Lens (Lens') +import Data.Lens.Record (prop) +import React.Halo.Task as Task +import Type.Proxy (Proxy(..)) + +type State = + { search :: Task.State SearchError Results + , query :: String + } + +searchLens :: Lens' State (Task.State SearchError Results) +searchLens = prop (Proxy :: Proxy "search") + +initialState = + { search: Task.idle + , query: "" + } +``` + +A policy body remains ordinary `HaloM` and returns `Either error result`. It may update other component state. Halo atomically stores a matching `Left` as `Failed` or `Right` as `Succeeded`: + +```purescript +Search query -> Task.supersede searchLens do + modify_ _ { query = query } + lift (Search.run query) + +CancelSearch -> Task.reset searchLens +``` + +Choose a policy by invocation semantics: + +- `once lens body` starts only from `Idle`; success and typed failure remain terminal until `reset`. +- `startIfInactive lens body` ignores a call while active, but starts from `Idle`, `Failed`, or `Succeeded`. +- `supersede lens body` makes every new call authoritative immediately. Prior work is fenced and cancellation is requested without waiting, so its finalizers may overlap the new body but cannot commit Halo state or begin another lifted application effect. +- `debounce lens milliseconds body` is trailing-edge latest-wins. Its private timer and body both render as `Active`; a new call cancels either phase. Nonpositive durations use a scheduled zero delay. +- `reset lens` publishes `Idle`, cancels active work, and waits for its Aff finalizers. Terminal state is cleared immediately. + +Render through the read-only projection: + +```purescript +case Task.toStatus state.search of + Task.Idle -> renderPrompt + Task.Active -> renderSpinner + Task.Failed error -> renderError error + Task.Succeeded results -> renderResults results +``` + +`Task.asStatus` is a standard read-only getter, and `_Idle`, `_Active`, `_Failed`, and `_Succeeded` are lawful prisms over `Task.Status`. `Task.toMaybe` returns only a succeeded result; `Task.isActive` covers both the private debounce timer and the executing body. + +Expected failures belong in `Either`. An unexpected exception returns the matching task to `Idle` and is reported through the latest `onError` as `ForkError`. Cancellation is neither a typed failure nor an unexpected error. Put retry policy in AppM and lift the already-retrying computation; when nested under `debounce`, the debounce timer runs once and AppM then owns its attempts. A retry loop must let Aff cancellation propagate rather than catching every exception. + +Task state is component-owned result storage, not a global cache. Calls do not retain an input or computation for later reruns. + ## Start and kill component processes `Halo.fork child` starts a process owned by the current React activation and returns a `ForkId`. The process may outlive the handler that created it: @@ -123,10 +181,13 @@ Choose cleanup according to the resource: - **Component process:** acquire and use the resource inside `fork` with an Aff finalizer. Deactivation requests cancellation of the fork. - **Event source:** return synchronous cleanup from `makeEmitter`; Halo runs it while deactivating the subscription scope. +- **Other synchronous resource:** call `registerCleanup cleanup`. Call `releaseCleanup id` to remove and run it early. - **User cancellation:** retain the `ForkId` and call `kill`, which waits for finalizers. - **Persistence:** save during normal application flow. Do not rely on unmount completing asynchronous persistence. -Deactivation first fences the activation, then runs subscription cleanup and requests cancellation of handlers and forks. React cannot wait for those Aff cancellations, but their finalizers cannot commit Halo state or begin new lifted effects after the fence. +`registerCleanup` accepts only `Effect Unit`, not `HaloM`, AppM, or `Aff`. `releaseCleanup` removes tracking before invoking the effect, so a throw is reported in the current root's error context and is not retried. Unknown and already released IDs are ignored. + +Deactivation first fences the activation, then attempts every generic cleanup and subscription cleanup before requesting cancellation of handlers, forks, and tasks. A cleanup throw is reported as `DeactivationError` through the latest `onError` without blocking the other resources. No ordering between generic and subscription cleanup is part of the API. React cannot wait for Aff cancellation, but finalizers cannot commit Halo state or begin new lifted effects after the fence. ## Run independent work in parallel diff --git a/spago.lock b/spago.lock index 01b0719..b516492 100644 --- a/spago.lock +++ b/spago.lock @@ -16,6 +16,7 @@ "ordered-collections", "parallel", "prelude", + "profunctor-lenses", "react-basic-hooks", "refs", "transformers", @@ -1548,6 +1549,34 @@ "tuples" ] }, + "profunctor-lenses": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-mVIYR3kEMHyO2m+VicJeZeVyDJ4IgvkMzDll3JdnRN4=", + "dependencies": [ + "arrays", + "bifunctors", + "const", + "control", + "distributive", + "either", + "foldable-traversable", + "foreign-object", + "functors", + "identity", + "lists", + "maybe", + "newtype", + "ordered-collections", + "partial", + "prelude", + "profunctor", + "record", + "safe-coerce", + "transformers", + "tuples" + ] + }, "react-basic": { "type": "registry", "version": "17.0.0", diff --git a/spago.yaml b/spago.yaml index 11d1a15..f9139a9 100644 --- a/spago.yaml +++ b/spago.yaml @@ -18,6 +18,7 @@ package: - ordered-collections - parallel - prelude + - profunctor-lenses - react-basic-hooks - refs - transformers diff --git a/src/React/Halo.purs b/src/React/Halo.purs index e3ad67b..26098f7 100644 --- a/src/React/Halo.purs +++ b/src/React/Halo.purs @@ -3,7 +3,7 @@ -- | A Halo component keeps application effects in a caller-defined monad `m`. -- | `component` or `useHalo` receives a natural transformation from `m` to -- | `Aff`, while `HaloM` adds component props, state, actions, cancellable --- | forks, subscriptions, and lifecycle ownership. +-- | forks, synchronous cleanup, subscriptions, and lifecycle ownership. -- | -- | Import this module for the intentional public API. Runtime constructors and -- | ownership records remain internal. @@ -14,6 +14,6 @@ module React.Halo import React.Halo.Component (ComponentSpec, component) as Exports import React.Halo.Handlers (Handlers, defaultHandlers) as Exports import React.Halo.Hook (HaloResult, HookSpec, UseHalo, useHalo) as Exports -import React.Halo.Internal.Runtime (HaloAp, HaloM, fork, getProps, kill, subscribe, subscribeWithId, unsubscribe) as Exports -import React.Halo.Internal.Types (ErrorContext(..), ForkId, SubscriptionId) as Exports +import React.Halo.Internal.Runtime (HaloAp, HaloM, fork, getProps, kill, registerCleanup, releaseCleanup, subscribe, subscribeWithId, unsubscribe) as Exports +import React.Halo.Internal.Types (CleanupId, ErrorContext(..), ForkId, SubscriptionId) as Exports import React.Halo.Subscription (Emitter, makeEmitter) as Exports diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index ee2de50..03b9749 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -10,6 +10,10 @@ module React.Halo.Internal.Runtime , fork , getProps , kill + , managedReset + , managedStart + , registerCleanup + , releaseCleanup , subscribe , subscribeWithId , syncSpec @@ -42,7 +46,7 @@ import Effect.Class (class MonadEffect, liftEffect) import Effect.Exception as Exception import Effect.Ref (Ref) import Effect.Ref as Ref -import React.Halo.Internal.Types (ErrorContext(..), ForkId(..), SubscriptionId(..)) +import React.Halo.Internal.Types (CleanupId(..), ErrorContext(..), ForkId(..), SubscriptionId(..)) import React.Halo.Subscription (Emitter) import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) @@ -141,6 +145,7 @@ newtype RunInAff m = RunInAff (m ~> Aff) newtype Runtime props state action m = Runtime { fresh :: Ref Int + , needsStatePublish :: Ref Boolean , props :: Ref props , runInAff :: Ref (RunInAff m) , scope :: Ref (Maybe Scope) @@ -151,6 +156,7 @@ newtype Runtime props state action m = Runtime newtype Scope = Scope { active :: Ref Boolean + , cleanups :: Ref (Map CleanupId (Effect Unit)) , forks :: Ref (Map ForkId Root) , generation :: Int , handlers :: Ref (Map Int Root) @@ -162,6 +168,7 @@ newtype Owner = Owner newtype Root = Root { fiber :: Fiber Unit + , onDeactivate :: Effect Unit , owner :: Owner } @@ -189,6 +196,7 @@ createRuntime -> Effect (Runtime props state action m) createRuntime runInAff input = do freshRef <- Ref.new 0 + needsStatePublish <- Ref.new false propsRef <- Ref.new input.initialProps runInAffRef <- Ref.new (RunInAff runInAff) scope <- Ref.new Nothing @@ -197,6 +205,7 @@ createRuntime runInAff input = do stateUpdate <- Ref.new input.stateUpdate pure $ Runtime { fresh: freshRef + , needsStatePublish , props: propsRef , runInAff: runInAffRef , scope @@ -228,11 +237,13 @@ activate runtime@(Runtime state) = do Nothing -> do generation <- fresh runtime active <- Ref.new true + cleanups <- Ref.new Map.empty forks <- Ref.new Map.empty handlers <- Ref.new Map.empty subscriptions <- Ref.new Map.empty - let scope = Scope { active, forks, generation, handlers, subscriptions } + let scope = Scope { active, cleanups, forks, generation, handlers, subscriptions } Ref.write (Just scope) state.scope + publishRuntimeState runtime spec <- Ref.read state.spec startHandler runtime scope ActivationError spec.handlers.onActivate @@ -247,13 +258,16 @@ deactivate (Runtime state) = do handlers <- takeRef current.handlers Map.empty forks <- takeRef current.forks Map.empty + cleanups <- takeRef current.cleanups Map.empty subscriptions <- takeRef current.subscriptions Map.empty let roots = Map.values handlers <> Map.values forks - -- Fence every root before invoking foreign cleanup or requesting - -- cooperative Aff cancellation. + -- Fence every root before normalizing managed state, invoking foreign + -- cleanup, or requesting cooperative Aff cancellation. traverse_ fenceRoot roots - cleanupResults <- traverse Exception.try (Map.values subscriptions) + traverse_ runRootDeactivation roots + cleanupResults <- traverse Exception.try + (Map.values cleanups <> Map.values subscriptions) traverse_ requestCancel roots -- A faulty external cleanup must not prevent any other cleanup request. @@ -324,9 +338,13 @@ fork child = HaloM $ ReaderT \execution -> do fid <- liftEffect $ ForkId <$> fresh execution.runtime current <- liftEffect $ isCurrent execution when current do - prepared <- liftEffect $ prepare execution.runInAff execution.runtime execution.scope (ForkError fid) child do - let Scope scope = execution.scope - Ref.modify_ (Map.delete fid) scope.forks + prepared <- liftEffect $ prepare execution.runInAff execution.runtime execution.scope (ForkError fid) child + { onComplete: do + let Scope scope = execution.scope + Ref.modify_ (Map.delete fid) scope.forks + , onDeactivate: pure unit + , onUnexpected: pure unit + } liftEffect do let Scope scope = execution.scope Ref.modify_ (Map.insert fid prepared.root) scope.forks @@ -361,6 +379,140 @@ kill fid = HaloM $ ReaderT \execution -> do ) root +-- Internal managed roots support state-focused lifecycle APIs without exposing +-- root identity. Claiming state, fencing prior work, registering the new root, +-- and opening its start gate happen in one synchronous runtime transaction. +managedStart + :: forall props state action m + . Maybe (Aff Unit) + -> ( Int + -> ForkId + -> state + -> Maybe + { cancel :: Maybe ForkId + , computation :: HaloM props state action m Unit + , onExit :: state -> Maybe state + , state :: state + } + ) + -> HaloM props state action m Unit +managedStart privateDelay claim = HaloM $ ReaderT \execution -> + liftEffect do + current <- isCurrent execution + when current do + fid <- ForkId <$> fresh execution.runtime + let Scope scope = execution.scope + oldState <- readRuntimeState execution.runtime + case claim scope.generation fid oldState of + Nothing -> pure unit + Just managed -> do + let + applyExit publish = applyManagedState publish execution.runtime managed.onExit + prepared <- prepare execution.runInAff execution.runtime execution.scope (ForkError fid) + (withPrivateDelay privateDelay managed.computation) + { onComplete: Ref.modify_ (Map.delete fid) scope.forks + , onDeactivate: applyExit false + , onUnexpected: applyExit true + } + previous <- Ref.modify' + ( \forks -> + { state: case managed.cancel of + Nothing -> forks + Just cancelId -> Map.delete cancelId forks + , value: managed.cancel >>= flip Map.lookup forks + } + ) + scope.forks + traverse_ fenceRoot previous + writeRuntimeState execution.runtime managed.state + Ref.modify_ (Map.insert fid prepared.root) scope.forks + update <- readStateUpdate execution.runtime + update managed.state + traverse_ requestCancel previous + prepared.start + +-- Stop managed work after atomically publishing its replacement state. A root +-- that resets itself is cancelled by unwinding its own fiber rather than trying +-- to join itself. +managedReset + :: forall props state action m + . ( Int + -> state + -> Maybe + { cancel :: Maybe ForkId + , state :: state + } + ) + -> HaloM props state action m Unit +managedReset transition = HaloM $ ReaderT \execution -> do + current <- liftEffect $ isCurrent execution + when current do + root <- liftEffect do + let Scope scope = execution.scope + oldState <- readRuntimeState execution.runtime + case transition scope.generation oldState of + Nothing -> pure Nothing + Just next -> do + previous <- Ref.modify' + ( \forks -> + { state: case next.cancel of + Nothing -> forks + Just cancelId -> Map.delete cancelId forks + , value: next.cancel >>= flip Map.lookup forks + } + ) + scope.forks + traverse_ fenceRoot previous + writeRuntimeState execution.runtime next.state + update <- readStateUpdate execution.runtime + update next.state + pure previous + traverse_ + ( \managedRoot -> + if sameOwner execution.owner managedRoot then + Aff.throwError scopeCancellationError + else cancelRootAff managedRoot + ) + root + +-- | Register synchronous `Effect` cleanup in the current activation scope. +-- | +-- | Deactivation attempts every remaining cleanup after fencing scope roots. +-- | Cleanup failures are isolated and reported as `DeactivationError` through +-- | the latest error callback. +registerCleanup + :: forall props state action m + . Effect Unit + -> HaloM props state action m CleanupId +registerCleanup cleanup = HaloM $ ReaderT \execution -> do + cid <- liftEffect $ CleanupId <$> fresh execution.runtime + current <- liftEffect $ isCurrent execution + when current do + liftEffect do + let Scope scope = execution.scope + Ref.modify_ (Map.insert cid cleanup) scope.cleanups + pure cid + +-- | Remove tracked cleanup before running it. Unknown or already released IDs +-- | are ignored. A throwing cleanup cannot be retried during deactivation and +-- | follows the error context of the handler or fork that releases it. +releaseCleanup + :: forall props state action m + . CleanupId + -> HaloM props state action m Unit +releaseCleanup cid = HaloM $ ReaderT \execution -> do + current <- liftEffect $ isCurrent execution + when current do + let Scope scope = execution.scope + cleanup <- liftEffect $ Ref.modify' + ( \cleanups -> + { state: Map.delete cid cleanups + , value: Map.lookup cid cleanups + } + ) + scope.cleanups + liftEffect $ traverse_ identity cleanup + -- | Register an action emitter in the current activation scope. -- | -- | Its synchronous cleanup runs on manual unsubscription or deactivation. @@ -420,8 +572,11 @@ startHandler startHandler runtime@(Runtime state) scope@(Scope current) context computation = do runId <- fresh runtime runInAff <- Ref.read state.runInAff - prepared <- prepare runInAff runtime scope context computation do - Ref.modify_ (Map.delete runId) current.handlers + prepared <- prepare runInAff runtime scope context computation + { onComplete: Ref.modify_ (Map.delete runId) current.handlers + , onDeactivate: pure unit + , onUnexpected: pure unit + } Ref.modify_ (Map.insert runId prepared.root) current.handlers prepared.start @@ -432,9 +587,12 @@ prepare -> Scope -> ErrorContext props action -> HaloM props state action m Unit - -> Effect Unit + -> { onComplete :: Effect Unit + , onDeactivate :: Effect Unit + , onUnexpected :: Effect Unit + } -> Effect Prepared -prepare runInAff runtime scope context computation onComplete = do +prepare runInAff runtime scope context computation hooks = do owner <- createOwner gate <- EffectAVar.empty fiber <- Aff.launchAff do @@ -443,7 +601,7 @@ prepare runInAff runtime scope context computation onComplete = do ( liftEffect do let Owner current = owner Ref.write false current.alive - onComplete + hooks.onComplete ) do outcome <- Aff.attempt $ Aff.supervise $ @@ -452,12 +610,13 @@ prepare runInAff runtime scope context computation onComplete = do Left error -> do current <- liftEffect $ isCurrent { context, owner, runInAff, runtime, scope } when current do + liftEffect hooks.onUnexpected let Runtime state = runtime spec <- liftEffect $ Ref.read state.spec liftEffect $ spec.onError context error Right _ -> pure unit pure - { root: Root { fiber, owner } + { root: Root { fiber, onDeactivate: hooks.onDeactivate, owner } , start: Aff.launchAff_ (AVar.put unit gate) } @@ -469,6 +628,53 @@ runHaloM runHaloM execution (HaloM computation) = case computation of ReaderT run -> run execution +withPrivateDelay + :: forall props state action m a + . Maybe (Aff Unit) + -> HaloM props state action m a + -> HaloM props state action m a +withPrivateDelay privateDelay computation = HaloM $ ReaderT \execution -> do + traverse_ identity privateDelay + runHaloM execution computation + +applyManagedState + :: forall props state action m + . Boolean + -> Runtime props state action m + -> (state -> Maybe state) + -> Effect Unit +applyManagedState publish (Runtime current) transition = do + oldState <- Ref.read current.state + case transition oldState of + Nothing -> pure unit + Just newState -> do + Ref.write newState current.state + if publish then do + update <- Ref.read current.stateUpdate + update newState + else Ref.write true current.needsStatePublish + +publishRuntimeState :: forall props state action m. Runtime props state action m -> Effect Unit +publishRuntimeState (Runtime runtime) = do + needsPublish <- Ref.read runtime.needsStatePublish + when needsPublish do + Ref.write false runtime.needsStatePublish + currentState <- Ref.read runtime.state + update <- Ref.read runtime.stateUpdate + update currentState + +readRuntimeState :: forall props state action m. Runtime props state action m -> Effect state +readRuntimeState (Runtime runtime) = Ref.read runtime.state + +writeRuntimeState :: forall props state action m. Runtime props state action m -> state -> Effect Unit +writeRuntimeState (Runtime runtime) = flip Ref.write runtime.state + +readStateUpdate + :: forall props state action m + . Runtime props state action m + -> Effect (state -> Effect Unit) +readStateUpdate (Runtime runtime) = Ref.read runtime.stateUpdate + createOwner :: Effect Owner createOwner = do alive <- Ref.new true @@ -479,6 +685,13 @@ fenceRoot (Root root) = do let Owner owner = root.owner Ref.write false owner.alive +runRootDeactivation :: Root -> Effect Unit +runRootDeactivation (Root root) = root.onDeactivate + +sameOwner :: Owner -> Root -> Boolean +sameOwner (Owner left) (Root right) = case right.owner of + Owner owner -> unsafeRefEq left.alive owner.alive + requestCancel :: Root -> Effect Unit requestCancel root = Aff.launchAff_ (cancelRootAff root) diff --git a/src/React/Halo/Internal/Task.purs b/src/React/Halo/Internal/Task.purs new file mode 100644 index 0000000..afb527f --- /dev/null +++ b/src/React/Halo/Internal/Task.purs @@ -0,0 +1,304 @@ +module React.Halo.Internal.Task + ( State + , Status(..) + , _Active + , _Failed + , _Idle + , _Succeeded + , asStatus + , debounce + , debounceWith + , idle + , isActive + , once + , reset + , startIfInactive + , supersede + , toMaybe + , toStatus + ) where + +import Prelude + +import Control.Monad.State.Class (state) +import Data.Either (Either(..)) +import Data.Lens (ALens', Getter', Prism', prism', to, withLens) +import Data.Maybe (Maybe(..), fromMaybe) +import Data.Tuple (Tuple(..)) +import Effect.Aff (Aff, Milliseconds(..)) +import Effect.Aff as Aff +import React.Halo.Internal.Runtime (HaloM, managedReset, managedStart) +import React.Halo.Internal.Types (ForkId) + +-- | Read-only task lifecycle projected from abstract component state. +data Status error result + = Idle + | Active + | Failed error + | Succeeded result + +derive instance eqStatus :: (Eq error, Eq result) => Eq (Status error result) + +instance showStatus :: (Show error, Show result) => Show (Status error result) where + show = case _ of + Idle -> "Idle" + Active -> "Active" + Failed error -> "(Failed " <> show error <> ")" + Succeeded result -> "(Succeeded " <> show result <> ")" + +data Lifecycle error result + = LifecycleIdle + | LifecycleActive Run + | LifecycleFailed error + | LifecycleSucceeded result + +type Run = + { forkId :: ForkId + , generation :: Int + , sequence :: Int + } + +-- | Task lifecycle stored inside component state. The constructor is kept +-- | private by `React.Halo.Task` because active values carry runtime ownership. +newtype State error result = State + { lifecycle :: Lifecycle error result + , nextSequence :: Int + } + +-- | Initial task state. +idle :: forall error result. State error result +idle = State { lifecycle: LifecycleIdle, nextSequence: 0 } + +-- | Project abstract task state to its public status. +toStatus :: forall error result. State error result -> Status error result +toStatus (State task) = case task.lifecycle of + LifecycleIdle -> Idle + LifecycleActive _ -> Active + LifecycleFailed error -> Failed error + LifecycleSucceeded result -> Succeeded result + +-- | Read-only optic from task state to public status. +asStatus :: forall error result. Getter' (State error result) (Status error result) +asStatus = to toStatus + +-- | Return a successful result, if present. +toMaybe :: forall error result. State error result -> Maybe result +toMaybe (State task) = case task.lifecycle of + LifecycleSucceeded result -> Just result + _ -> Nothing + +-- | Test whether a debounce timer or task body is active. +isActive :: forall error result. State error result -> Boolean +isActive (State task) = case task.lifecycle of + LifecycleActive _ -> true + _ -> false + +_Idle :: forall error result. Prism' (Status error result) Unit +_Idle = prism' (const Idle) case _ of + Idle -> Just unit + _ -> Nothing + +_Active :: forall error result. Prism' (Status error result) Unit +_Active = prism' (const Active) case _ of + Active -> Just unit + _ -> Nothing + +_Failed :: forall error result. Prism' (Status error result) error +_Failed = prism' Failed case _ of + Failed error -> Just error + _ -> Nothing + +_Succeeded :: forall error result. Prism' (Status error result) result +_Succeeded = prism' Succeeded case _ of + Succeeded result -> Just result + _ -> Nothing + +data Policy + = Once + | IfInactive + | Supersede + +-- | Start only from `Idle`. Typed failure and success remain terminal until +-- | `reset`. +once + :: forall props componentState action m error result + . ALens' componentState (State error result) + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +once = launch Once Nothing + +-- | Start from `Idle`, `Failed`, or `Succeeded`, but preserve active work. +startIfInactive + :: forall props componentState action m error result + . ALens' componentState (State error result) + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +startIfInactive = launch IfInactive Nothing + +-- | Make a new invocation authoritative immediately, fencing and requesting +-- | cancellation of prior managed work without waiting for its finalizers. +supersede + :: forall props componentState action m error result + . ALens' componentState (State error result) + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +supersede = launch Supersede Nothing + +-- | Trailing-edge latest-wins task invocation. The private cancellable timer and +-- | the executing body both project to `Active`. +debounce + :: forall props componentState action m error result + . ALens' componentState (State error result) + -> Milliseconds + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +debounce = debounceWith Aff.delay + +-- Internal deterministic scheduler seam used by runtime tests. +debounceWith + :: forall props componentState action m error result + . (Milliseconds -> Aff Unit) + -> ALens' componentState (State error result) + -> Milliseconds + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +debounceWith schedule target duration = + launch Supersede (Just (schedule (nonNegative duration))) target + +-- | Clear terminal state or cancel active work. Active cancellation is fenced +-- | synchronously and waits for Aff finalizers before returning. +reset + :: forall props componentState action m error result + . ALens' componentState (State error result) + -> HaloM props componentState action m Unit +reset target = withLens target \getTask setTask -> + managedReset \generation componentState -> + let + State task = getTask componentState + nextState = State (task { lifecycle = LifecycleIdle }) + replacement = setTask componentState nextState + in + case task.lifecycle of + LifecycleIdle -> Nothing + LifecycleActive run -> Just + { cancel: if run.generation == generation then Just run.forkId else Nothing + , state: replacement + } + LifecycleFailed _ -> Just { cancel: Nothing, state: replacement } + LifecycleSucceeded _ -> Just { cancel: Nothing, state: replacement } + +launch + :: forall props componentState action m error result + . Policy + -> Maybe (Aff Unit) + -> ALens' componentState (State error result) + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +launch policy privateDelay target body = withLens target \getTask setTask -> + managedStart privateDelay \generation forkId componentState -> do + claimed <- claim policy generation forkId (getTask componentState) + pure + { cancel: claimed.cancel + , computation: complete getTask setTask claimed.run body + , onExit: clear getTask setTask claimed.run + , state: setTask componentState claimed.state + } + +claim + :: forall error result + . Policy + -> Int + -> ForkId + -> State error result + -> Maybe + { cancel :: Maybe ForkId + , run :: Run + , state :: State error result + } +claim policy generation forkId (State task) = do + let + currentRun = case task.lifecycle of + LifecycleActive run | run.generation == generation -> Just run + _ -> Nothing + canStart = case policy of + Once -> case task.lifecycle of + LifecycleIdle -> true + LifecycleActive run -> run.generation /= generation + _ -> false + IfInactive -> case currentRun of + Just _ -> false + Nothing -> true + Supersede -> true + if canStart then do + let + run = + { forkId + , generation + , sequence: task.nextSequence + } + nextState = State + { lifecycle: LifecycleActive run + , nextSequence: task.nextSequence + 1 + } + pure + { cancel: case policy of + Supersede -> _.forkId <$> currentRun + _ -> Nothing + , run + , state: nextState + } + else Nothing + +complete + :: forall props componentState action m error result + . (componentState -> State error result) + -> (componentState -> State error result -> componentState) + -> Run + -> HaloM props componentState action m (Either error result) + -> HaloM props componentState action m Unit +complete getTask setTask run body = do + outcome <- body + state \componentState -> + let + replacement = updateMatching run outcome (getTask componentState) + in + Tuple unit $ fromMaybe componentState (setTask componentState <$> replacement) + +clear + :: forall componentState error result + . (componentState -> State error result) + -> (componentState -> State error result -> componentState) + -> Run + -> componentState + -> Maybe componentState +clear getTask setTask run componentState = + setTask componentState <$> clearMatching run (getTask componentState) + +updateMatching + :: forall error result + . Run + -> Either error result + -> State error result + -> Maybe (State error result) +updateMatching run outcome (State task) = case task.lifecycle of + LifecycleActive current | sameRun run current -> Just $ State + ( task + { lifecycle = case outcome of + Left error -> LifecycleFailed error + Right result -> LifecycleSucceeded result + } + ) + _ -> Nothing + +clearMatching :: forall error result. Run -> State error result -> Maybe (State error result) +clearMatching run (State task) = case task.lifecycle of + LifecycleActive current | sameRun run current -> + Just $ State (task { lifecycle = LifecycleIdle }) + _ -> Nothing + +sameRun :: Run -> Run -> Boolean +sameRun left right = + left.generation == right.generation && left.sequence == right.sequence + +nonNegative :: Milliseconds -> Milliseconds +nonNegative (Milliseconds duration) = Milliseconds (max 0.0 duration) diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index 459d7e7..672eff0 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -1,11 +1,22 @@ module React.Halo.Internal.Types - ( ErrorContext(..) + ( CleanupId(..) + , ErrorContext(..) , ForkId(..) , SubscriptionId(..) ) where import Prelude +-- | Identifies synchronous cleanup in one React activation. Its constructor is +-- | hidden from the root `React.Halo` API. +newtype CleanupId = CleanupId Int + +derive newtype instance eqCleanupId :: Eq CleanupId + +derive newtype instance ordCleanupId :: Ord CleanupId + +derive newtype instance showCleanupId :: Show CleanupId + -- | Identifies the component-owned computation or cleanup whose unexpected -- | failure reached `onError`. -- | diff --git a/src/React/Halo/Task.purs b/src/React/Halo/Task.purs new file mode 100644 index 0000000..b056450 --- /dev/null +++ b/src/React/Halo/Task.purs @@ -0,0 +1,33 @@ +-- | Typed lifecycle state for component-owned Halo work. +-- | +-- | Import this module qualified. A `State error result` is stored inside +-- | component state and located with a standard lens; it is not a computation, +-- | key, or cache definition. Policy bodies remain ordinary `HaloM` values that +-- | return `Either error result`. +-- | +-- | The mutable representation is abstract because active state carries hidden +-- | run ownership. Render through `toStatus`, `asStatus`, `toMaybe`, or +-- | `isActive`. Expected `Left` values become `Failed`; unexpected exceptions +-- | return the task to `Idle` and follow Halo's normal `ForkError` routing. +module React.Halo.Task + ( module Exports + ) where + +import React.Halo.Internal.Task + ( State + , Status(..) + , _Active + , _Failed + , _Idle + , _Succeeded + , asStatus + , debounce + , idle + , isActive + , once + , reset + , startIfInactive + , supersede + , toMaybe + , toStatus + ) as Exports diff --git a/test/Main.purs b/test/Main.purs index 2dc209c..fcb7f0a 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -6,6 +6,7 @@ import Effect (Effect) import Test.Halo.ScopeHandlerSpec as ScopeHandlerSpec import Test.Halo.RuntimeSpec as RuntimeSpec import Test.Halo.SubscriptionErrorSpec as SubscriptionErrorSpec +import Test.Halo.TaskSpec as TaskSpec import Test.Spec.Reporter (consoleReporter) import Test.Spec.Runner.Node (runSpecAndExitProcess) @@ -14,3 +15,4 @@ main = runSpecAndExitProcess [ consoleReporter ] do RuntimeSpec.spec ScopeHandlerSpec.spec SubscriptionErrorSpec.spec + TaskSpec.spec diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index 4b8d0f7..cf164e2 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -3,10 +3,10 @@ module Test.Halo.DocExamples where import Prelude import Control.Monad.Reader (ReaderT, ask, runReaderT) -import Control.Monad.State (gets, modify_) import Control.Monad.Trans.Class (lift) -import Data.Foldable (traverse_) -import Data.Maybe (Maybe(..)) +import Data.Either (Either(..)) +import Data.Lens (Lens') +import Data.Lens.Record (prop) import Effect.Aff (Aff) import Effect.Aff.Class (class MonadAff, liftAff) import Effect.Class (class MonadEffect) @@ -16,6 +16,8 @@ import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) import React.Basic.Hooks (Component, Hook) import React.Halo as Halo +import React.Halo.Task as Task +import Type.Proxy (Proxy(..)) type Env = { loadGreeting :: Aff String } @@ -40,11 +42,12 @@ loadGreeting = AppM do type Props = { title :: String } type State = - { fiber :: Maybe Halo.ForkId - , loading :: Boolean - , result :: Maybe String + { greeting :: Task.State String String } +greetingLens :: Lens' State (Task.State String String) +greetingLens = prop (Proxy :: Proxy "greeting") + data Action = Load | Cancel @@ -54,23 +57,15 @@ type UI a = Halo.HaloM Props State Action AppM a handlers :: Halo.Handlers Props State Action AppM handlers = Halo.defaultHandlers { onAction = case _ of - Load -> do - previous <- gets _.fiber - traverse_ Halo.kill previous - fiber <- Halo.fork do - modify_ _ { loading = true, result = Nothing } - result <- lift loadGreeting - modify_ _ { loading = false, result = Just result } - modify_ _ { fiber = Just fiber } - Cancel -> do - previous <- gets _.fiber - traverse_ Halo.kill previous - modify_ _ { fiber = Nothing, loading = false } + Load -> Task.supersede greetingLens do + greeting <- lift loadGreeting + pure (Right greeting) + Cancel -> Task.reset greetingLens } loadButton :: Env -> Component Props loadButton env = Halo.component "LoadButton" (runAppM env) - { initialState: \_ -> { fiber: Nothing, loading: false, result: Nothing } + { initialState: \_ -> { greeting: Task.idle } , handlers , onError: \_ error -> Console.error $ "Unexpected Halo error: " <> message error @@ -79,15 +74,17 @@ loadButton env = Halo.component "LoadButton" (runAppM env) [ R.text props.title , R.button { onClick: capture_ (dispatch Load) - , children: [ R.text if state.loading then "Restart" else "Load" ] + , children: [ R.text if Task.isActive state.greeting then "Restart" else "Load" ] } , R.button { onClick: capture_ (dispatch Cancel) , children: [ R.text "Cancel" ] } - , R.text $ case state.result of - Nothing -> if state.loading then "Loading…" else "Not loaded" - Just greeting -> greeting + , R.text $ case Task.toStatus state.greeting of + Task.Idle -> "Not loaded" + Task.Active -> "Loading…" + Task.Failed error -> error + Task.Succeeded greeting -> greeting ] } @@ -97,7 +94,7 @@ useExample -> Hook (Halo.UseHalo Props State Action AppM) (Halo.HaloResult State Action) useExample env props = Halo.useHalo (runAppM env) { props - , initialState: { fiber: Nothing, loading: false, result: Nothing } + , initialState: { greeting: Task.idle } , handlers , onError: \_ _ -> pure unit } diff --git a/test/Test/Halo/GuideExamples.purs b/test/Test/Halo/GuideExamples.purs index b265e63..8ca6059 100644 --- a/test/Test/Halo/GuideExamples.purs +++ b/test/Test/Halo/GuideExamples.purs @@ -5,9 +5,14 @@ import Prelude import Control.Monad.State (modify_) import Control.Monad.Trans.Class (lift) import Control.Parallel (parallel, sequential) +import Data.Either (Either(..)) +import Data.Lens (Lens') +import Data.Lens.Record (prop) import Data.Tuple (Tuple(..)) -import Effect.Aff (Aff) +import Effect.Aff (Aff, Milliseconds(..)) import React.Halo as Halo +import React.Halo.Task as Task +import Type.Proxy (Proxy(..)) type DashboardState = { feed :: Int @@ -30,3 +35,37 @@ simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) simpleSubscription :: Halo.HaloM Unit Unit SimpleAction Aff Unit simpleSubscription = void $ Halo.subscribeWithId \_ -> simpleEmitter + +genericCleanup :: Halo.HaloM Unit Unit SimpleAction Aff Unit +genericCleanup = do + cleanupId <- Halo.registerCleanup (pure unit) + Halo.releaseCleanup cleanupId + +type SearchState = + { query :: String + , search :: Task.State String Int + } + +data SearchAction = Search String | CancelSearch + +searchLens :: Lens' SearchState (Task.State String Int) +searchLens = prop (Proxy :: Proxy "search") + +retryingSearch :: String -> Aff (Either String Int) +retryingSearch _ = pure (Right 1) + +searchHandler + :: SearchAction + -> Halo.HaloM Unit SearchState SearchAction Aff Unit +searchHandler = case _ of + Search query -> Task.debounce searchLens (Milliseconds 250.0) do + modify_ _ { query = query } + lift (retryingSearch query) + CancelSearch -> Task.reset searchLens + +renderSearch :: SearchState -> String +renderSearch state = case Task.toStatus state.search of + Task.Idle -> "Search" + Task.Active -> "Searching" + Task.Failed error -> error + Task.Succeeded _ -> "Done" diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index e81a115..5c605ec 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -3,7 +3,7 @@ module Test.Halo.SubscriptionErrorSpec (spec) where import Prelude import Control.Monad.State (get, modify_, put) -import Data.Foldable (traverse_) +import Data.Foldable (foldl, traverse_) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Aff (Aff) @@ -16,8 +16,8 @@ import Effect.Class (liftEffect) import Effect.Exception as Exception import Effect.Ref as Ref import React.Halo.Handlers (Handlers, defaultHandlers) -import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, fork, subscribe, syncSpec, unsubscribe) -import React.Halo.Internal.Types (ErrorContext(..), ForkId, SubscriptionId) +import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, fork, registerCleanup, releaseCleanup, subscribe, syncSpec, unsubscribe) +import React.Halo.Internal.Types (CleanupId(..), ErrorContext(..), ForkId, SubscriptionId) import React.Halo.Subscription (Emitter, makeEmitter) import Test.Halo.Helpers (Gate, await, makeGate, release, waitForGate) import Test.Spec (Spec, describe, it) @@ -27,7 +27,127 @@ identityAff :: Aff ~> Aff identityAff = identity spec :: Spec Unit -spec = describe "subscriptions and errors" do +spec = describe "subscriptions, cleanup, and errors" do + it "releases generic cleanup once and ignores unknown IDs" do + cleanupCount <- liftEffect $ Ref.new 0 + registered <- liftEffect EffectAVar.empty + released <- liftEffect EffectAVar.empty + unknownReleased <- liftEffect EffectAVar.empty + runtime <- liftEffect $ createRuntime identityAff + { initialProps: unit + , initialState: Nothing + , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } + , stateUpdate: \_ -> pure unit + } + + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (RegisterCleanup (Ref.modify_ (_ + 1) cleanupCount) registered) + cid <- await "generic cleanup registration" registered + liftEffect $ dispatch runtime (ReleaseCleanup cid released) + void $ await "manual generic cleanup release" released + liftEffect $ dispatch runtime (ReleaseCleanup (CleanupId 999_999) unknownReleased) + void $ await "unknown generic cleanup release" unknownReleased + liftEffect $ dispatch runtime (ReleaseCleanup cid released) + void $ await "already released generic cleanup" released + liftEffect $ deactivate runtime + actual <- liftEffect $ Ref.read cleanupCount + actual `shouldEqual` 1 + + it "does not retry a throwing manual cleanup release" do + cleanupRuns <- liftEffect $ Ref.new 0 + registered <- liftEffect EffectAVar.empty + releaseFailed <- liftEffect EffectAVar.empty + errors <- liftEffect $ Ref.new [] + runtime <- liftEffect $ createRuntime identityAff + { initialProps: unit + , initialState: Nothing + , spec: + { handlers: subscriptionHandlers + , onError: \context error -> do + let + label = case context of + ActionError (ReleaseCleanup _ _) -> "release" + _ -> "wrong context" + Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) errors + void $ EffectAVar.tryPut unit releaseFailed + } + , stateUpdate: \_ -> pure unit + } + + liftEffect do + activate runtime + dispatch runtime + ( RegisterCleanup + (Ref.modify_ (_ + 1) cleanupRuns *> Exception.throw "manual cleanup failed") + registered + ) + cid <- await "throwing cleanup registration" registered + completed <- liftEffect EffectAVar.empty + liftEffect $ dispatch runtime (ReleaseCleanup cid completed) + void $ await "throwing manual cleanup error" releaseFailed + liftEffect $ deactivate runtime + runs <- liftEffect $ Ref.read cleanupRuns + actual <- liftEffect $ Ref.read errors + runs `shouldEqual` 1 + actual `shouldEqual` [ "release: manual cleanup failed" ] + + it "rejects cleanup registration retained by a stale activation" do + cleanupCount <- liftEffect $ Ref.new 0 + gate <- liftEffect makeGate + runtime <- liftEffect $ + ( createRuntime identityAff + { initialProps: unit + , initialState: unit + , spec: + { handlers: staleCleanupHandlers + , onError: \_ _ -> pure unit + } + , stateUpdate: \_ -> pure unit + } :: Effect (Runtime Unit Unit StaleCleanupAction Aff) + ) + + liftEffect do + activate runtime + dispatch runtime (RegisterAfterCancellation gate (Ref.modify_ (_ + 1) cleanupCount)) + void $ await "stale cleanup handler" gate.started + liftEffect $ deactivate runtime + void $ await "stale cleanup cancellation" gate.settled + liftEffect do + activate runtime + deactivate runtime + actual <- liftEffect $ Ref.read cleanupCount + actual `shouldEqual` 0 + + it "keeps generic cleanup IDs scoped to one StrictMode activation" do + cleanupCount <- liftEffect $ Ref.new 0 + firstRegistered <- liftEffect EffectAVar.empty + staleReleased <- liftEffect EffectAVar.empty + secondRegistered <- liftEffect EffectAVar.empty + runtime <- liftEffect $ createRuntime identityAff + { initialProps: unit + , initialState: Nothing + , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } + , stateUpdate: \_ -> pure unit + } + + liftEffect do + activate runtime + dispatch runtime (RegisterCleanup (Ref.modify_ (_ + 1) cleanupCount) firstRegistered) + staleId <- await "first activation cleanup" firstRegistered + liftEffect do + deactivate runtime + activate runtime + dispatch runtime (ReleaseCleanup staleId staleReleased) + void $ await "stale cleanup release" staleReleased + liftEffect $ dispatch runtime + (RegisterCleanup (Ref.modify_ (_ + 1) cleanupCount) secondRegistered) + void $ await "second activation cleanup" secondRegistered + liftEffect $ deactivate runtime + actual <- liftEffect $ Ref.read cleanupCount + actual `shouldEqual` 2 + it "removes a manual unsubscribe from scope tracking" do cleanupCount <- liftEffect $ Ref.new 0 started <- liftEffect EffectAVar.empty @@ -78,25 +198,29 @@ spec = describe "subscriptions and errors" do cleaned <- liftEffect $ Ref.read cleanupCount cleaned `shouldEqual` 1 - it "isolates throwing cleanup and reports DeactivationError" do + it "isolates cleanup failures across generic and subscription resources" do cleaned <- liftEffect $ Ref.new 0 + oldErrors <- liftEffect $ Ref.new [] cleanupErrors <- liftEffect $ Ref.new [] badStarted <- liftEffect EffectAVar.empty goodStarted <- liftEffect EffectAVar.empty + badCleanupRegistered <- liftEffect EffectAVar.empty + goodCleanupRegistered <- liftEffect EffectAVar.empty state <- liftEffect $ Ref.new Nothing gate <- liftEffect makeGate let - badEmitter = makeEmitter \_ -> pure $ Exception.throw "cleanup failed" + badEmitter = makeEmitter \_ -> pure $ Exception.throw "subscription cleanup failed" goodEmitter = makeEmitter \_ -> pure $ Ref.modify_ (_ + 1) cleaned + onCleanupError target context error = case context of + DeactivationError -> Ref.modify_ (_ <> [ Exception.message error ]) target + _ -> Ref.modify_ (_ <> [ "wrong error context" ]) target runtime <- liftEffect $ createRuntime identityAff { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers - , onError: \context error -> case context of - DeactivationError -> Ref.modify_ (_ <> [ Exception.message error ]) cleanupErrors - _ -> Ref.modify_ (_ <> [ "wrong error context" ]) cleanupErrors + , onError: onCleanupError oldErrors } , stateUpdate: flip Ref.write state } @@ -107,16 +231,34 @@ spec = describe "subscriptions and errors" do void $ await "failing subscription setup" badStarted liftEffect $ dispatch runtime (Start goodEmitter goodStarted) void $ await "successful subscription setup" goodStarted + liftEffect $ dispatch runtime + (RegisterCleanup (Exception.throw "generic cleanup failed") badCleanupRegistered) + void $ await "failing generic cleanup setup" badCleanupRegistered + liftEffect $ dispatch runtime + (RegisterCleanup (Ref.modify_ (_ + 1) cleaned) goodCleanupRegistered) + void $ await "successful generic cleanup setup" goodCleanupRegistered liftEffect $ dispatch runtime (Block gate) void $ await "running action handler" gate.started - liftEffect $ deactivate runtime + liftEffect do + syncSpec runtime identityAff + { spec: + { handlers: subscriptionHandlers + , onError: onCleanupError cleanupErrors + } + , stateUpdate: flip Ref.write state + } + deactivate runtime void $ await "running action cancellation" gate.settled cleanupCount <- liftEffect $ Ref.read cleaned - cleanupCount `shouldEqual` 1 + cleanupCount `shouldEqual` 2 + previous <- liftEffect $ Ref.read oldErrors + previous `shouldEqual` [] errors <- liftEffect $ Ref.read cleanupErrors - errors `shouldEqual` [ "cleanup failed" ] + foldl (\count _ -> count + 1) 0 errors `shouldEqual` 2 + foldl (\found message -> found || message == "generic cleanup failed") false errors `shouldEqual` true + foldl (\found message -> found || message == "subscription cleanup failed") false errors `shouldEqual` true it "rejects a callback retained by a stale activation" do callback <- liftEffect $ Ref.new Nothing @@ -227,6 +369,8 @@ data SubscriptionAction = Start (Emitter SubscriptionAction) (AVar Unit) | Stop (AVar Unit) | Block Gate + | RegisterCleanup (Effect Unit) (AVar CleanupId) + | ReleaseCleanup CleanupId (AVar Unit) type SubscriptionState = Maybe SubscriptionId @@ -243,6 +387,21 @@ subscriptionHandlers = defaultHandlers put Nothing liftAff $ void $ AVar.tryPut unit completed Block gate -> liftAff $ waitForGate gate + RegisterCleanup cleanup registered -> do + cid <- registerCleanup cleanup + liftAff $ void $ AVar.tryPut cid registered + ReleaseCleanup cid completed -> do + releaseCleanup cid + liftAff $ void $ AVar.tryPut unit completed + } + +data StaleCleanupAction = RegisterAfterCancellation Gate (Effect Unit) + +staleCleanupHandlers :: Handlers Unit Unit StaleCleanupAction Aff +staleCleanupHandlers = defaultHandlers + { onAction = \(RegisterAfterCancellation gate cleanup) -> do + liftAff $ Aff.catchError (waitForGate gate) (\_ -> pure unit) + void $ registerCleanup cleanup } data StaleAction diff --git a/test/Test/Halo/TaskSpec.purs b/test/Test/Halo/TaskSpec.purs new file mode 100644 index 0000000..fa9dd46 --- /dev/null +++ b/test/Test/Halo/TaskSpec.purs @@ -0,0 +1,526 @@ +module Test.Halo.TaskSpec (spec) where + +import Prelude + +import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.State (modify_) +import Control.Monad.Trans.Class (lift) +import Data.Either (Either(..)) +import Data.Lens (Lens', preview, review) +import Data.Lens.Record (prop) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Aff (Aff, Milliseconds(..)) +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.AVar (AVar) +import Effect.AVar as EffectAVar +import Effect.Class (liftEffect) +import Effect.Exception as Exception +import Effect.Ref as Ref +import React.Halo.Handlers (Handlers, defaultHandlers) +import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, syncSpec) +import React.Halo.Internal.Task as TaskInternal +import React.Halo.Internal.Types (ErrorContext(..)) +import React.Halo.Task as Task +import Test.Halo.Helpers (Gate, await, makeGate, release, shouldNotHaveStarted, waitForGate) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) +import Type.Proxy (Proxy(..)) + +type ComponentState = + { elsewhere :: Int + , task :: Task.State String Int + } + +taskLens :: Lens' ComponentState (Task.State String Int) +taskLens = prop (Proxy :: Proxy "task") + +data Body + = WaitBody Gate (Either String Int) + | UpdateBody Gate Int (Either String Int) + | UnexpectedBody Gate + | CancellableBody Gate Gate (Ref.Ref Boolean) + +type Timer = + { duration :: AVar Milliseconds + , gate :: Gate + } + +data Action + = RunOnce Body (AVar Unit) + | RunStartIfInactive Body (AVar Unit) + | RunStartTwice Body Body (AVar Unit) + | RunSupersede Body (AVar Unit) + | RunDebounce Timer Milliseconds Body (AVar Unit) + | Reset (AVar Unit) + +type UI a = HaloM Unit ComponentState Action Aff a + +handlers :: Handlers Unit ComponentState Action Aff +handlers = defaultHandlers + { onAction = case _ of + RunOnce body launched -> do + Task.once taskLens (runBody body) + signal launched + RunStartIfInactive body launched -> do + Task.startIfInactive taskLens (runBody body) + signal launched + RunStartTwice first second launched -> do + Task.startIfInactive taskLens (runBody first) + Task.startIfInactive taskLens (runBody second) + signal launched + RunSupersede body launched -> do + Task.supersede taskLens (runBody body) + signal launched + RunDebounce timer duration body launched -> do + TaskInternal.debounceWith (runTimer timer) taskLens duration (runBody body) + signal launched + Reset completed -> do + Task.reset taskLens + signal completed + } + +runBody :: Body -> UI (Either String Int) +runBody = case _ of + WaitBody gate outcome -> do + liftAff $ waitForGate gate + pure outcome + UpdateBody gate amount outcome -> do + liftAff $ waitForGate gate + modify_ \state -> state { elsewhere = state.elsewhere + amount } + pure outcome + UnexpectedBody gate -> do + liftAff $ waitForGate gate + liftAff $ Aff.throwError (Aff.error "task boom") + CancellableBody work finalizer externalWitness -> do + liftAff $ Aff.catchError + (Aff.finally (waitForGate finalizer) (waitForGate work)) + (\_ -> pure unit) + modify_ \state -> state { elsewhere = state.elsewhere + 100 } + liftEffect $ Ref.write true externalWitness + pure (Right 999) + +runTimer :: Timer -> Milliseconds -> Aff Unit +runTimer timer duration = do + AVar.put duration timer.duration + waitForGate timer.gate + +signal :: AVar Unit -> UI Unit +signal completed = liftAff $ void $ AVar.tryPut unit completed + +type Harness = + { runtime :: Runtime Unit ComponentState Action Aff + , setterCalls :: Ref.Ref Int + , state :: Ref.Ref ComponentState + , updates :: AVar (Task.Status String Int) + } + +makeHarness + :: (ErrorContext Unit Action -> Exception.Error -> Effect Unit) + -> Aff Harness +makeHarness onError = liftEffect do + state <- Ref.new initialState + updates <- EffectAVar.empty + setterCalls <- Ref.new 0 + runtime <- createRuntime identity + { initialProps: unit + , initialState + , spec: { handlers, onError } + , stateUpdate: updateState state updates setterCalls + } + activate runtime + pure { runtime, setterCalls, state, updates } + +initialState :: ComponentState +initialState = { elsewhere: 0, task: Task.idle } + +updateState + :: Ref.Ref ComponentState + -> AVar (Task.Status String Int) + -> Ref.Ref Int + -> ComponentState + -> Effect Unit +updateState state updates setterCalls next = do + previous <- Ref.read state + Ref.write next state + Ref.modify_ (_ + 1) setterCalls + let + previousStatus = Task.toStatus previous.task + nextStatus = Task.toStatus next.task + when (previousStatus /= nextStatus) do + void $ EffectAVar.tryPut nextStatus updates + +awaitStatus + :: String + -> Task.Status String Int + -> AVar (Task.Status String Int) + -> Aff Unit +awaitStatus label expected updates = do + actual <- await label updates + if actual == expected then pure unit + else awaitStatus label expected updates + +makeTimer :: Effect Timer +makeTimer = do + gate <- makeGate + duration <- EffectAVar.empty + pure { duration, gate } + +statusOf :: Harness -> Effect (Task.Status String Int) +statusOf harness = Task.toStatus <<< _.task <$> Ref.read harness.state + +spec :: Spec Unit +spec = describe "state-focused tasks" do + it "projects status through helpers and lawful prisms" do + Task.toStatus (Task.idle :: Task.State String Int) `shouldEqual` Task.Idle + Task.toMaybe (Task.idle :: Task.State String Int) `shouldEqual` Nothing + Task.isActive (Task.idle :: Task.State String Int) `shouldEqual` false + preview (Task.asStatus <<< Task._Idle) (Task.idle :: Task.State String Int) `shouldEqual` Just unit + preview Task._Idle (Task.Idle :: Task.Status String Int) `shouldEqual` Just unit + preview Task._Active (Task.Active :: Task.Status String Int) `shouldEqual` Just unit + preview Task._Failed (Task.Failed "no" :: Task.Status String Int) `shouldEqual` Just "no" + preview Task._Succeeded (Task.Succeeded 4 :: Task.Status String Int) `shouldEqual` Just 4 + review Task._Failed "bad" `shouldEqual` (Task.Failed "bad" :: Task.Status String Int) + review Task._Succeeded 5 `shouldEqual` (Task.Succeeded 5 :: Task.Status String Int) + + it "keeps once terminal until reset and stores typed outcomes" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + first <- liftEffect makeGate + ignored <- liftEffect makeGate + failed <- liftEffect makeGate + firstLaunched <- liftEffect EffectAVar.empty + ignoredLaunched <- liftEffect EffectAVar.empty + failedLaunched <- liftEffect EffectAVar.empty + resetDone <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody first (Right 1)) firstLaunched) + void $ await "once launch" firstLaunched + void $ await "once body" first.started + awaitStatus "once active" Task.Active harness.updates + + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody ignored (Right 2)) ignoredLaunched) + void $ await "ignored active once" ignoredLaunched + shouldNotHaveStarted ignored + + release first + void $ await "once body settlement" first.settled + awaitStatus "once success" (Task.Succeeded 1) harness.updates + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody ignored (Right 2)) ignoredLaunched) + shouldNotHaveStarted ignored + + liftEffect $ dispatch harness.runtime (Reset resetDone) + void $ await "terminal reset" resetDone + awaitStatus "reset idle" Task.Idle harness.updates + + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody failed (Left "expected")) failedLaunched) + void $ await "once after reset" failedLaunched + void $ await "failed body" failed.started + awaitStatus "once active after reset" Task.Active harness.updates + release failed + awaitStatus "typed failure" (Task.Failed "expected") harness.updates + + another <- liftEffect makeGate + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody another (Right 3)) ignoredLaunched) + shouldNotHaveStarted another + + it "starts from terminal states only when inactive and claims duplicates atomically" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + first <- liftEffect makeGate + ignored <- liftEffect makeGate + second <- liftEffect makeGate + third <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunStartTwice (WaitBody first (Left "first")) (WaitBody ignored (Right 99)) launched) + void $ await "same-turn start calls" launched + void $ await "first inactive body" first.started + shouldNotHaveStarted ignored + awaitStatus "start active" Task.Active harness.updates + release first + awaitStatus "start typed failure" (Task.Failed "first") harness.updates + + liftEffect $ dispatch harness.runtime (RunStartIfInactive (WaitBody second (Right 2)) launched) + void $ await "terminal rerun body" second.started + awaitStatus "terminal replaced" Task.Active harness.updates + release second + awaitStatus "terminal success" (Task.Succeeded 2) harness.updates + + liftEffect $ dispatch harness.runtime (RunStartIfInactive (WaitBody third (Right 3)) launched) + void $ await "success rerun body" third.started + awaitStatus "success replaced" Task.Active harness.updates + release third + awaitStatus "second success" (Task.Succeeded 3) harness.updates + + it "allows bodies to update unrelated component state" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + gate <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + liftEffect $ dispatch harness.runtime (RunStartIfInactive (UpdateBody gate 7 (Right 8)) launched) + void $ await "updating body" gate.started + awaitStatus "updating task active" Task.Active harness.updates + release gate + awaitStatus "updating task success" (Task.Succeeded 8) harness.updates + state <- liftEffect $ Ref.read harness.state + state.elsewhere `shouldEqual` 7 + + it "supersedes immediately while old finalizers overlap and stale effects stay fenced" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + oldWork <- liftEffect makeGate + oldFinalizer <- liftEffect makeGate + newWork <- liftEffect makeGate + externalWitness <- liftEffect $ Ref.new false + oldLaunched <- liftEffect EffectAVar.empty + newLaunched <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunSupersede (CancellableBody oldWork oldFinalizer externalWitness) oldLaunched) + void $ await "old task launch" oldLaunched + void $ await "old task body" oldWork.started + awaitStatus "old task active" Task.Active harness.updates + + liftEffect $ dispatch harness.runtime (RunSupersede (WaitBody newWork (Right 2)) newLaunched) + void $ await "new superseding launch" newLaunched + void $ await "old finalizer overlap" oldFinalizer.started + void $ await "new task during old finalizer" newWork.started + active <- liftEffect $ statusOf harness + active `shouldEqual` Task.Active + + release newWork + awaitStatus "new task success" (Task.Succeeded 2) harness.updates + externalBeforeRelease <- liftEffect $ Ref.read externalWitness + externalBeforeRelease `shouldEqual` false + stateBeforeRelease <- liftEffect $ Ref.read harness.state + stateBeforeRelease.elsewhere `shouldEqual` 0 + + release oldFinalizer + void $ await "old finalizer settlement" oldFinalizer.settled + externalAfterRelease <- liftEffect $ Ref.read externalWitness + externalAfterRelease `shouldEqual` false + stateAfterRelease <- liftEffect $ Ref.read harness.state + stateAfterRelease.elsewhere `shouldEqual` 0 + Task.toStatus stateAfterRelease.task `shouldEqual` Task.Succeeded 2 + + it "reset publishes Idle immediately and waits for finalizers" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + work <- liftEffect makeGate + finalizer <- liftEffect makeGate + externalWitness <- liftEffect $ Ref.new false + launched <- liftEffect EffectAVar.empty + resetDone <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunSupersede (CancellableBody work finalizer externalWitness) launched) + void $ await "resettable task launch" launched + void $ await "resettable task body" work.started + awaitStatus "resettable active" Task.Active harness.updates + + liftEffect $ dispatch harness.runtime (Reset resetDone) + awaitStatus "reset publishes idle" Task.Idle harness.updates + void $ await "reset finalizer start" finalizer.started + completionBeforeFinalizer <- liftEffect $ EffectAVar.tryTake resetDone + completionBeforeFinalizer `shouldEqual` Nothing + + release finalizer + void $ await "reset completion" resetDone + witness <- liftEffect $ Ref.read externalWitness + witness `shouldEqual` false + status <- liftEffect $ statusOf harness + status `shouldEqual` Task.Idle + + it "returns unexpected failures to Idle and routes ForkError" do + raised <- liftEffect EffectAVar.empty + errors <- liftEffect $ Ref.new [] + harness <- makeHarness \context error -> do + let + label = case context of + ForkError _ -> "fork" + _ -> "wrong context" + Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) errors + void $ EffectAVar.tryPut unit raised + Aff.finally (liftEffect $ deactivate harness.runtime) do + gate <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + liftEffect $ dispatch harness.runtime (RunSupersede (UnexpectedBody gate) launched) + void $ await "unexpected task launch" launched + void $ await "unexpected task body" gate.started + awaitStatus "unexpected task active" Task.Active harness.updates + release gate + awaitStatus "unexpected task idle" Task.Idle harness.updates + void $ await "unexpected task onError" raised + actual <- liftEffect $ Ref.read errors + actual `shouldEqual` [ "fork: task boom" ] + + it "debounces with a private trailing timer and keeps only the latest result" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + firstTimer <- liftEffect makeTimer + secondTimer <- liftEffect makeTimer + firstBody <- liftEffect makeGate + secondBody <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunDebounce firstTimer (Milliseconds 50.0) (WaitBody firstBody (Right 1)) launched) + void $ await "first debounce launch" launched + void $ await "first debounce timer" firstTimer.gate.started + awaitStatus "first debounce active" Task.Active harness.updates + + liftEffect $ dispatch harness.runtime + (RunDebounce secondTimer (Milliseconds 50.0) (WaitBody secondBody (Right 2)) launched) + void $ await "second debounce timer" secondTimer.gate.started + void $ await "cancelled first debounce" firstTimer.gate.settled + active <- liftEffect $ statusOf harness + active `shouldEqual` Task.Active + shouldNotHaveStarted firstBody + + release secondTimer.gate + void $ await "latest debounced body" secondBody.started + release secondBody + awaitStatus "latest debounce success" (Task.Succeeded 2) harness.updates + shouldNotHaveStarted firstBody + + it "normalizes nonpositive debounce duration and reset cancels its timer" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + timer <- liftEffect makeTimer + body <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + resetDone <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunDebounce timer (Milliseconds (-10.0)) (WaitBody body (Right 1)) launched) + observed <- await "normalized debounce duration" timer.duration + observed `shouldEqual` Milliseconds 0.0 + void $ await "nonpositive debounce timer" timer.gate.started + awaitStatus "nonpositive debounce active" Task.Active harness.updates + shouldNotHaveStarted body + + liftEffect $ dispatch harness.runtime (Reset resetDone) + void $ await "debounce reset" resetDone + void $ await "debounce timer cancellation" timer.gate.settled + awaitStatus "debounce reset idle" Task.Idle harness.updates + shouldNotHaveStarted body + + it "normalizes active state without a cleanup setter and republishes before reactivation work" do + harness <- makeHarness \_ _ -> pure unit + first <- liftEffect makeGate + second <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody first (Right 1)) launched) + void $ await "pre-deactivation task" first.started + awaitStatus "pre-deactivation active" Task.Active harness.updates + callsBefore <- liftEffect $ Ref.read harness.setterCalls + + liftEffect $ deactivate harness.runtime + void $ await "deactivated task cancellation" first.settled + callsAfterCleanup <- liftEffect $ Ref.read harness.setterCalls + callsAfterCleanup `shouldEqual` callsBefore + + liftEffect $ activate harness.runtime + awaitStatus "reactivation idle publication" Task.Idle harness.updates + liftEffect $ dispatch harness.runtime (RunOnce (WaitBody second (Right 2)) launched) + void $ await "reactivated once task" second.started + awaitStatus "reactivated task active" Task.Active harness.updates + release second + awaitStatus "reactivated task success" (Task.Succeeded 2) harness.updates + + ignored <- liftEffect makeGate + liftEffect do + deactivate harness.runtime + activate harness.runtime + dispatch harness.runtime (RunOnce (WaitBody ignored (Right 3)) launched) + shouldNotHaveStarted ignored + terminal <- liftEffect $ statusOf harness + terminal `shouldEqual` Task.Succeeded 2 + liftEffect $ deactivate harness.runtime + + it "uses the latest state setter for managed completion" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + gate <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + newState <- liftEffect $ Ref.new initialState + liftEffect $ dispatch harness.runtime (RunSupersede (WaitBody gate (Right 7)) launched) + void $ await "setter task body" gate.started + awaitStatus "setter task active" Task.Active harness.updates + + liftEffect $ syncSpec harness.runtime identity + { spec: { handlers, onError: \_ _ -> pure unit } + , stateUpdate: flip Ref.write newState + } + release gate + void $ await "setter task settlement" gate.settled + current <- liftEffect $ Ref.read newState + Task.toStatus current.task `shouldEqual` Task.Succeeded 7 + old <- liftEffect $ Ref.read harness.state + Task.toStatus old.task `shouldEqual` Task.Active + + snapshotSpec + +newtype AppM a = AppM (ReaderT Int Aff a) + +derive newtype instance functorAppM :: Functor AppM +derive newtype instance applyAppM :: Apply AppM +derive newtype instance applicativeAppM :: Applicative AppM +derive newtype instance bindAppM :: Bind AppM +derive newtype instance monadAppM :: Monad AppM + +runAppM :: Int -> AppM ~> Aff +runAppM environment (AppM computation) = runReaderT computation environment + +readEnvironment :: AppM Int +readEnvironment = AppM ask + +type SnapshotState = { task :: Task.State String Int } + +snapshotLens :: Lens' SnapshotState (Task.State String Int) +snapshotLens = prop (Proxy :: Proxy "task") + +data SnapshotAction = LaunchSnapshot Gate Gate (AVar Int) + +snapshotHandlers :: Handlers Unit SnapshotState SnapshotAction AppM +snapshotHandlers = defaultHandlers + { onAction = \(LaunchSnapshot handlerGate bodyGate result) -> do + lift $ AppM $ lift $ waitForGate handlerGate + Task.supersede snapshotLens do + lift $ AppM $ lift $ waitForGate bodyGate + environment <- lift readEnvironment + lift $ AppM $ lift $ void $ AVar.tryPut environment result + pure (Right environment) + } + +snapshotSpec :: Spec Unit +snapshotSpec = describe "managed task interpreter snapshots" do + it "inherits the launching handler's interpreter" do + handlerGate <- liftEffect makeGate + bodyGate <- liftEffect makeGate + result <- liftEffect EffectAVar.empty + runtime <- liftEffect $ createRuntime (runAppM 1) + { initialProps: unit + , initialState: { task: Task.idle } + , spec: { handlers: snapshotHandlers, onError: \_ _ -> pure unit } + , stateUpdate: \_ -> pure unit + } + Aff.finally (liftEffect $ deactivate runtime) do + liftEffect do + activate runtime + dispatch runtime (LaunchSnapshot handlerGate bodyGate result) + void $ await "snapshot handler" handlerGate.started + liftEffect $ syncSpec runtime (runAppM 2) + { spec: { handlers: snapshotHandlers, onError: \_ _ -> pure unit } + , stateUpdate: \_ -> pure unit + } + release handlerGate + void $ await "snapshot task body" bodyGate.started + release bodyGate + actual <- await "snapshot task environment" result + actual `shouldEqual` 1 From e8c3a41b9b2a02850fc1de65ffe163b4fc7e0a42 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Wed, 2 Sep 2026 01:50:18 +0900 Subject: [PATCH 12/16] Brand task slots and publish authority views --- README.md | 20 +- docs/architecture.md | 24 +- docs/guide.md | 33 +- src/React/Halo/Component.purs | 6 +- src/React/Halo/Hook.purs | 29 +- src/React/Halo/Internal/Runtime.purs | 402 ++++++++++++++----- src/React/Halo/Internal/Task.purs | 301 +++----------- src/React/Halo/Internal/Task/Types.purs | 370 ++++++++++++++++++ src/React/Halo/Internal/Types.purs | 6 + src/React/Halo/Task.purs | 40 +- test/Test/Halo/DocExamples.purs | 17 +- test/Test/Halo/GuideExamples.purs | 11 +- test/Test/Halo/RuntimeSpec.purs | 6 +- test/Test/Halo/ScopeHandlerSpec.purs | 16 +- test/Test/Halo/SubscriptionErrorSpec.purs | 24 +- test/Test/Halo/TaskSpec.purs | 455 ++++++++++++++++++++-- 16 files changed, 1287 insertions(+), 473 deletions(-) create mode 100644 src/React/Halo/Internal/Task/Types.purs diff --git a/README.md b/README.md index b0a56e2..729c4d0 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ type State = greetingLens :: Lens' State (Task.State String String) greetingLens = prop (Proxy :: Proxy "greeting") +greetingSlot :: Task.Slot "greeting" State String String +greetingSlot = Task.slot (Proxy :: Proxy "greeting") greetingLens + data Action = Load | Cancel type UI a = Halo.HaloM Props State Action AppM a @@ -101,15 +104,15 @@ type UI a = Halo.HaloM Props State Action AppM a handlers :: Halo.Handlers Props State Action AppM handlers = Halo.defaultHandlers { onAction = case _ of - Load -> Task.supersede greetingLens do + Load -> Task.supersede greetingSlot do greeting <- lift loadGreeting pure (Right greeting) - Cancel -> Task.reset greetingLens + Cancel -> Task.reset greetingSlot } ``` -A task body is ordinary `HaloM` and returns `Either error result`. `supersede` makes the new invocation authoritative immediately; `reset` cancels active work and waits for its Aff finalizers. Rendering sees only `Idle`, `Active`, `Failed error`, or `Succeeded result`; hidden run identity prevents stale completion from overwriting newer state. +A slot is an opaque identity-bearing optic for one task field. The type-level name distinguishes same-typed fields; it does not store a body, input, or cancellation key. A task body is ordinary `HaloM` and returns `Either error result`. `supersede` makes the new invocation authoritative immediately; `reset` cancels active work and waits for its Aff finalizers. `lift` is `Control.Monad.Trans.Class.lift`. Each handler captures the interpreter current when it starts. Managed tasks and forks inherit their launching handler's interpreter, even if React renders with a newer interpreter before their bodies begin. @@ -119,22 +122,22 @@ Supply the interpreter when creating the component: loadButton :: Env -> Component Props loadButton env = Halo.component "LoadButton" (runAppM env) { initialState: \_ -> - { greeting: Task.idle } + { greeting: Task.idle greetingSlot } , handlers , onError: \_ error -> Console.error $ "Unexpected Halo error: " <> message error - , render: \{ props, state, dispatch } -> + , render: \{ props, tasks, dispatch } -> R.div_ [ R.text props.title , R.button { onClick: capture_ (dispatch Load) - , children: [ R.text if Task.isActive state.greeting then "Restart" else "Load" ] + , children: [ R.text if Task.isActive tasks greetingSlot then "Restart" else "Load" ] } , R.button { onClick: capture_ (dispatch Cancel) , children: [ R.text "Cancel" ] } - , R.text $ case Task.toStatus state.greeting of + , R.text $ case Task.toStatus tasks greetingSlot of Task.Idle -> "Not loaded" Task.Active -> "Loading…" Task.Failed error -> error @@ -143,6 +146,8 @@ loadButton env = Halo.component "LoadButton" (runAppM env) } ``` +`state` and `tasks` come from one coherent render snapshot. `Task.State` values can be copied as ordinary component data, but only the canonical slot with matching runtime authority projects `Active`; stale, foreign, or cross-slot active values project `Idle`. + `initialState` receives the initial props once per mount. Later prop changes call `handlers.onPropsChange`; they do not recreate state. Use the hook form when Halo shares a component with other hooks: @@ -156,6 +161,7 @@ halo <- Halo.useHalo (runAppM env) } -- halo.state +-- halo.tasks -- halo.dispatch ``` diff --git a/docs/architecture.md b/docs/architecture.md index 4413b2f..63b9bbf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,7 +16,7 @@ The interpreter must return the owned `Aff` computation. An interpreter that det ## React delegates ownership to the runtime -[`React.Halo.Component`](../src/React/Halo/Component.purs) computes initial state from the initial props and delegates to `useHalo`. The renderer receives current props, Halo state, and an action dispatcher. +[`React.Halo.Component`](../src/React/Halo/Component.purs) computes initial state from the initial props and delegates to `useHalo`. The renderer receives current props, Halo state, its coherent immutable task view, and an action dispatcher. [`React.Halo.Hook`](../src/React/Halo/Hook.purs) creates one internal runtime for the hook instance and connects it to React effects: @@ -24,11 +24,11 @@ The interpreter must return the owned `Aff` computation. An interpreter that det 2. activate the runtime and return synchronous deactivation as effect cleanup; and 3. publish prop changes to the runtime. -The hook returns only current state and `dispatch`. [`React.Halo.Internal.Runtime`](../src/React/Halo/Internal/Runtime.purs) owns fibers, activation scopes, subscriptions, state fencing, and error routing. +The hook returns current `state`, an immutable task-authority `tasks` view, and `dispatch`. State and task view are published through one React setter as a coherent snapshot. [`React.Halo.Internal.Runtime`](../src/React/Halo/Internal/Runtime.purs) owns fibers, activation scopes, task authority, subscriptions, state fencing, and error routing. ## Each activation has a generation -An active runtime holds one scope with a unique generation, an active flag, and maps for handler roots, component forks, generic cleanup, and subscriptions. Activation is idempotent while that scope is current. +An active runtime holds one scope with a unique generation, an active flag, and maps for handler roots, component forks, task authority, generic cleanup, and subscriptions. Activation is idempotent while that scope is current. Heterogeneous task-slot bindings persist separately for the runtime lifetime; active authority does not. Deactivation marks the scope inactive and clears it from the runtime before foreign cleanup or Aff cancellation begins. A later activation creates fresh maps and a new generation. Currency checks require both an active matching generation and a live root owner, so work retained from an earlier generation cannot affect a reactivated component. @@ -42,9 +42,15 @@ Every `onActivate`, `onPropsChange`, and `onAction` invocation starts an indepen Root completion removes only that root's current map entry. IDs are fresh within the runtime, so stale completion cannot remove newer work. -Managed task policies use the same fork map and root ownership rather than a scheduler or key registry. In one synchronous transaction the runtime claims lensed task state, fences a superseded root, prepares and records the replacement behind a gate, publishes `Active`, requests old cancellation, and opens the new gate. The task body inherits the launching root's interpreter. Hidden run identity makes delayed timers and completions conditional on the authoritative run. +Managed task policies use the same fork map and root ownership rather than a separate coordination subsystem. A `Task.Slot` combines a type-level brand with a canonical lens. First use registers an erased binding that can inspect and normalize that focus without retaining a body, input, or fork handle. Temporary semantic probes verify that one brand maps to one focus and one focus to one brand; probes are never published. A collision throws in the calling root's existing error context before state mutation or cancellation. -`Task.State` projects only `Idle`, `Active`, `Failed`, or `Succeeded`. Debounce timing remains private: an owned Aff timer precedes the body in the same managed root. A typed `Either` completion updates the lensed state atomically. An unexpected current failure clears the matching run to `Idle` before existing `ForkError` routing. +Activation authority maps a registered slot brand to an exact token containing runtime identity, activation generation, and `ForkId`. In one synchronous transaction the runtime reconciles registered slots, claims authority, fences a superseded root, prepares and records the replacement behind a gate, publishes state and view, requests old cancellation, and opens the new gate. The task body inherits the launching root's interpreter. + +`Task.State` is freely copyable and does not itself prove ownership. `Task.View` is an immutable pair of the published state snapshot and authority map. Slot-aware projection reports `Active` only when the canonical focus and authority contain the exact token; copied, stale, cross-slot, and cross-runtime active values report `Idle`. + +Every generic `MonadState` write reconciles registered bindings before publication. Preserving the exact token is ordinary. Replacing it with correctly branded idle or terminal state removes/fences authority while preserving the requested value. Foreign or stale active state is normalized to branded idle and can never supply a fork to cancel. Typed completion and unexpected failure use exact canonical-focus and authority checks. + +Debounce timing remains private: an owned Aff timer precedes the body in the same managed root, and both phases project `Active`. A typed `Either` completion removes authority and stores `Failed` or `Succeeded` atomically. An unexpected current failure clears the exact run to `Idle` before existing `ForkError` routing. ## Fences precede cancellation @@ -52,7 +58,7 @@ Cancellation is cooperative, but ownership loss is synchronous. For explicit `kill`, the runtime removes the fork from tracking, fences its owner, requests Aff cancellation, and waits for the fiber and its Aff finalizers before returning. An unknown or completed `ForkId` is a no-op. -React deactivation cannot wait asynchronously. It invalidates the scope; takes all tracked roots, generic cleanup, and subscriptions; and fences every root. Managed active task state is then normalized to `Idle` in the runtime state without calling React's setter during cleanup. Every synchronous cleanup is attempted before cancellation is requested for handler, fork, and task fibers. Cleanup failures are reported only after the runtime has attempted the rest of the cleanup work. +React deactivation cannot wait asynchronously. It invalidates the scope; takes all tracked roots, generic cleanup, and subscriptions; and fences every root. Activation authority is cleared and persistent bindings normalize active task state to `Idle` in the runtime state without calling React's setter during cleanup. Every synchronous cleanup is attempted before cancellation is requested for handler, fork, and task fibers. Cleanup failures are reported only after the runtime has attempted the rest of the cleanup work. If deactivation normalized task state, the next activation publishes that runtime state through the latest React setter before starting `onActivate`. Terminal task outcomes have no active root and persist. This ordering keeps StrictMode replay state coherent without adding an asynchronous deactivation callback. @@ -61,7 +67,7 @@ The fences protect two important boundaries: - `MonadState` may still compute a stale operation's return value, but it cannot update stored state or call React's state setter. - A later `lift` from a stale root fails with Halo's internal cancellation error before invoking `m ~> Aff`. Catching the initial Aff cancellation therefore cannot start a newly lifted application effect. -Capabilities that create or remove forks and subscriptions also check currency. Cancellation cannot undo an external effect that already happened inside an application computation; application writes must still use appropriate idempotency or retry semantics. +Capabilities that create or remove forks, task roots, cleanup, and subscriptions also check currency. Cancellation cannot undo an external effect that already happened inside an application computation; application writes must still use appropriate idempotency or retry semantics. ## Synchronous cleanup stays activation-scoped @@ -83,7 +89,7 @@ Concurrent Halo state writes have nondeterministic ordering and can overwrite on ## Errors use the current reporting callback -Each root carries the context assigned at launch: `ActivationError`, `PropsChangeError previousProps`, `ActionError action`, or `ForkError id`. Managed task bodies use `ForkError` because they are component-owned roots without public task keys. An unexpected failure is reported only while that root and scope are still current. The runtime reads the latest `onError` callback at reporting time, so a render can update reporting without changing an already-running root's interpreter. +Each root carries the context assigned at launch: `ActivationError`, `PropsChangeError previousProps`, `ActionError action`, or `ForkError id`. Managed task bodies use `ForkError` because they are component-owned roots without separate public run identity. An unexpected failure is reported only while that root and scope are still current. The runtime reads the latest `onError` callback at reporting time, so a render can update reporting without changing an already-running root's interpreter. Subscription cleanup failures use `DeactivationError`. Halo-initiated cancellation and stale-lift failures are suppressed because the owner has already been fenced. @@ -94,7 +100,7 @@ The deterministic runtime tests exercise ownership without mounting a real DOM f - [`RuntimeSpec`](../test/Test/Halo/RuntimeSpec.purs) covers AppM interpretation, handler and fork interpreter snapshots, the stale-lift fence, and direct parallel execution. - [`ScopeHandlerSpec`](../test/Test/Halo/ScopeHandlerSpec.purs) covers activation generations, StrictMode reactivation, props, handler and fork ownership, explicit kill, finalizer waiting, and stale capability/state rejection. - [`SubscriptionErrorSpec`](../test/Test/Halo/SubscriptionErrorSpec.purs) covers generic and subscription cleanup isolation, manual release, stale activation IDs and emitter callbacks, error contexts, and latest error-handler selection. -- [`TaskSpec`](../test/Test/Halo/TaskSpec.purs) covers task optics and policies, atomic claims, supersession and reset, private debounce scheduling, stale effect fences, deactivation normalization, current setters, and inherited interpreters. +- [`TaskSpec`](../test/Test/Halo/TaskSpec.purs) covers branded slots, view projection, collision detection, state-copy and stale-snapshot reconciliation, cross-runtime authority, policies, atomic claims, supersession and reset, private debounce scheduling, stale effect fences, two-slot deactivation normalization, current setters, and inherited interpreters. - [`DocExamples`](../test/Test/Halo/DocExamples.purs) compile-checks the complete component, hook, AppM, and task example. - [`GuideExamples`](../test/Test/Halo/GuideExamples.purs) compile-checks the guide's task, parallel, subscription, and cleanup examples. diff --git a/docs/guide.md b/docs/guide.md index 3257459..7521385 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -96,7 +96,7 @@ Capture props before asynchronous work when that work must use one render's valu ## Store typed task outcomes in component state -Import `React.Halo.Task` qualified when component state should retain the lifecycle and typed result of owned work. `Task.State error result` is abstract because it includes hidden cancellation identity. Locate it with a standard lens: +Import `React.Halo.Task` qualified when component state should retain the lifecycle and typed result of owned work. Pair each `Task.State error result` field with an opaque branded slot built from a type-level name and standard lens: ```purescript import Data.Lens (Lens') @@ -112,41 +112,48 @@ type State = searchLens :: Lens' State (Task.State SearchError Results) searchLens = prop (Proxy :: Proxy "search") +searchSlot :: Task.Slot "search" State SearchError Results +searchSlot = Task.slot (Proxy :: Proxy "search") searchLens + initialState = - { search: Task.idle + { search: Task.idle searchSlot , query: "" } ``` +`Task.State` remains ordinary, freely copyable component data; it cannot by itself prove ownership of a live root. `Task.View` validates the canonical slot against an immutable runtime authority snapshot. Copying active state to another slot, restoring stale state, or supplying state from another runtime therefore projects `Idle` and cannot cross-cancel authoritative work. + +A slot name must identify exactly one state focus for the runtime lifetime, and one focus cannot use multiple names. Halo validates the lawful lenses on first use. A collision fails before state mutation or cancellation and follows the current handler or fork error context. + A policy body remains ordinary `HaloM` and returns `Either error result`. It may update other component state. Halo atomically stores a matching `Left` as `Failed` or `Right` as `Succeeded`: ```purescript -Search query -> Task.supersede searchLens do +Search query -> Task.supersede searchSlot do modify_ _ { query = query } lift (Search.run query) -CancelSearch -> Task.reset searchLens +CancelSearch -> Task.reset searchSlot ``` Choose a policy by invocation semantics: -- `once lens body` starts only from `Idle`; success and typed failure remain terminal until `reset`. -- `startIfInactive lens body` ignores a call while active, but starts from `Idle`, `Failed`, or `Succeeded`. -- `supersede lens body` makes every new call authoritative immediately. Prior work is fenced and cancellation is requested without waiting, so its finalizers may overlap the new body but cannot commit Halo state or begin another lifted application effect. -- `debounce lens milliseconds body` is trailing-edge latest-wins. Its private timer and body both render as `Active`; a new call cancels either phase. Nonpositive durations use a scheduled zero delay. -- `reset lens` publishes `Idle`, cancels active work, and waits for its Aff finalizers. Terminal state is cleared immediately. +- `once slot body` starts only from `Idle`; success and typed failure remain terminal until `reset`. +- `startIfInactive slot body` ignores a call while active, but starts from `Idle`, `Failed`, or `Succeeded`. +- `supersede slot body` makes every new call authoritative immediately. Prior work is fenced and cancellation is requested without waiting, so its finalizers may overlap the new body but cannot commit Halo state or begin another lifted application effect. +- `debounce slot milliseconds body` is trailing-edge latest-wins. Its private timer and body both render as `Active`; a new call cancels either phase. Nonpositive durations use a scheduled zero delay. +- `reset slot` publishes `Idle`, cancels active work, and waits for its Aff finalizers. Terminal state is cleared immediately. Render through the read-only projection: ```purescript -case Task.toStatus state.search of +case Task.toStatus tasks searchSlot of Task.Idle -> renderPrompt Task.Active -> renderSpinner Task.Failed error -> renderError error Task.Succeeded results -> renderResults results ``` -`Task.asStatus` is a standard read-only getter, and `_Idle`, `_Active`, `_Failed`, and `_Succeeded` are lawful prisms over `Task.Status`. `Task.toMaybe` returns only a succeeded result; `Task.isActive` covers both the private debounce timer and the executing body. +A component renderer receives `tasks :: Task.View State` beside `state`; `useHalo` returns the same view. The state and view are one coherent immutable React snapshot. `Task.toMaybe tasks slot` returns only a succeeded result, and `Task.isActive tasks slot` covers both the private debounce timer and executing body. `_Idle`, `_Active`, `_Failed`, and `_Succeeded` remain lawful prisms over `Task.Status`. Expected failures belong in `Either`. An unexpected exception returns the matching task to `Idle` and is reported through the latest `onError` as `ForkError`. Cancellation is neither a typed failure nor an unexpected error. Put retry policy in AppM and lift the already-retrying computation; when nested under `debounce`, the debounce timer runs once and AppM then owns its attempts. A retry loop must let Aff cancellation propagate rather than catching every exception. @@ -267,9 +274,9 @@ Halo selects the latest `onError` callback when reporting a failure. Expected do ## Choose `component` or `useHalo` -Use `Halo.component` when Halo owns the complete component. Its renderer receives `{ props, state, dispatch }`. `initialState` receives initial props once per mount; synchronize later prop changes in `onPropsChange`. +Use `Halo.component` when Halo owns the complete component. Its renderer receives `{ props, state, tasks, dispatch }`. `initialState` receives initial props once per mount; synchronize later prop changes in `onPropsChange`. -Use `Halo.useHalo` when other React hooks share the render function. It accepts the same application interpreter and returns `{ state, dispatch }`. +Use `Halo.useHalo` when other React hooks share the render function. It accepts the same application interpreter and returns `{ state, tasks, dispatch }`. ## Common mistakes diff --git a/src/React/Halo/Component.purs b/src/React/Halo/Component.purs index 6011c88..fe519c9 100644 --- a/src/React/Halo/Component.purs +++ b/src/React/Halo/Component.purs @@ -11,13 +11,15 @@ import React.Basic.Hooks (Component, JSX) import React.Basic.Hooks as React import React.Halo.Handlers (Handlers) import React.Halo.Hook (useHalo) +import React.Halo.Internal.Task.Types (View) import React.Halo.Internal.Types (ErrorContext) -- | Configuration for a complete Halo-owned React component. -- | -- | `initialState` receives the initial props once per mount. Later prop changes -- | start `handlers.onPropsChange` and do not recreate state. The renderer --- | receives current props and state plus synchronous action dispatch. +-- | receives current props plus one coherent state/task-view snapshot and +-- | synchronous action dispatch. type ComponentSpec props state action m = { handlers :: Handlers props state action m , initialState :: props -> state @@ -26,6 +28,7 @@ type ComponentSpec props state action m = { dispatch :: action -> Effect Unit , props :: props , state :: state + , tasks :: View state } -> JSX } @@ -54,4 +57,5 @@ component name runInAff spec = { dispatch: halo.dispatch , props , state: halo.state + , tasks: halo.tasks } diff --git a/src/React/Halo/Hook.purs b/src/React/Halo/Hook.purs index 423a4b6..dadcf94 100644 --- a/src/React/Halo/Hook.purs +++ b/src/React/Halo/Hook.purs @@ -16,6 +16,8 @@ import React.Basic.Hooks (Hook, UseEffect, UseMemo, UseState) import React.Basic.Hooks as React import React.Halo.Handlers (Handlers) import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, syncSpec, updateProps) +import React.Halo.Internal.Task.Types (View) +import React.Halo.Internal.Task.Types as Task import React.Halo.Internal.Types (ErrorContext) -- | Configuration for `useHalo`. @@ -31,12 +33,13 @@ type HookSpec props state action m = , props :: props } --- | Current component state and synchronous action dispatch exposed to --- | rendering code. Dispatch starts an independent handler root while the --- | current React activation is active. +-- | Coherent component state, immutable task-authority view, and synchronous +-- | action dispatch exposed to rendering code. Dispatch starts an independent +-- | handler root while the current React activation is active. type HaloResult state action = { dispatch :: action -> Effect Unit , state :: state + , tasks :: View state } newtype UseHalo props state action m hooks = UseHalo @@ -44,7 +47,7 @@ newtype UseHalo props state action m hooks = UseHalo ( UseEffect Unit ( UseEffect Unit ( UseMemo Unit (Runtime props state action m) - (UseState state hooks) + (UseState { state :: state, tasks :: View state } hooks) ) ) ) @@ -60,9 +63,9 @@ derive instance newtypeUseHalo :: Newtype (UseHalo props state action m hooks) _ -- | interpreter; existing roots retain their snapshot, and a fork inherits the -- | snapshot of the root that launches it. -- | --- | Effect cleanup fences the activation, runs subscription cleanup, and --- | requests cancellation of every handler and fork. A StrictMode setup replay --- | creates a fresh usable activation. +-- | Effect cleanup fences the activation, normalizes managed task state, runs +-- | synchronous cleanup, and requests cancellation of every handler and fork. +-- | A StrictMode setup replay publishes normalized state before new work. useHalo :: forall props state action m . (m ~> Aff) @@ -70,18 +73,21 @@ useHalo -> Hook (UseHalo props state action m) (HaloResult state action) useHalo runInAff { props, initialState, handlers, onError } = React.coerceHook React.do - state /\ setState <- React.useState' initialState + snapshot /\ setSnapshot <- React.useState' + { state: initialState + , tasks: Task.emptyView initialState + } runtime <- React.useMemo unit \_ -> unsafePerformEffect $ createRuntime runInAff { initialProps: props , initialState , spec: { handlers, onError } - , stateUpdate: setState + , stateUpdate: \state tasks -> setSnapshot { state, tasks } } React.useEffectAlways do syncSpec runtime runInAff { spec: { handlers, onError } - , stateUpdate: setState + , stateUpdate: \state tasks -> setSnapshot { state, tasks } } pure mempty React.useEffectOnce do @@ -92,5 +98,6 @@ useHalo runInAff { props, initialState, handlers, onError } = pure mempty pure { dispatch: dispatch runtime - , state + , state: snapshot.state + , tasks: snapshot.tasks } diff --git a/src/React/Halo/Internal/Runtime.purs b/src/React/Halo/Internal/Runtime.purs index 03b9749..5af769f 100644 --- a/src/React/Halo/Internal/Runtime.purs +++ b/src/React/Halo/Internal/Runtime.purs @@ -10,6 +10,7 @@ module React.Halo.Internal.Runtime , fork , getProps , kill + , managedComplete , managedReset , managedStart , registerCleanup @@ -30,7 +31,7 @@ import Control.Monad.Trans.Class (class MonadTrans, lift) import Control.Monad.Writer (class MonadTell, tell) import Control.Parallel (class Parallel, parallel, sequential) import Data.Either (Either(..)) -import Data.Foldable (traverse_) +import Data.Foldable (foldM, foldl, traverse_) import Data.Map (Map) import Data.Map as Map import Data.Maybe (Maybe(..)) @@ -46,7 +47,9 @@ import Effect.Class (class MonadEffect, liftEffect) import Effect.Exception as Exception import Effect.Ref (Ref) import Effect.Ref as Ref -import React.Halo.Internal.Types (CleanupId(..), ErrorContext(..), ForkId(..), SubscriptionId(..)) +import React.Halo.Internal.Task.Types (Binding, Token, View) +import React.Halo.Internal.Task.Types as Task +import React.Halo.Internal.Types (CleanupId(..), ErrorContext(..), ForkId(..), RuntimeId(..), SubscriptionId(..)) import React.Halo.Subscription (Emitter) import React.Halo.Subscription as Subscription import Unsafe.Reference (unsafeRefEq) @@ -118,9 +121,7 @@ instance monadStateHaloM :: MonadState state (HaloM props state action m) where let Tuple result newState = updateState oldState if current then do unless (unsafeRefEq oldState newState) do - Ref.write newState runtime.state - update <- Ref.read runtime.stateUpdate - update newState + commitReconciledState execution newState pure result else pure result @@ -144,18 +145,21 @@ type RuntimeSpec props state action m = newtype RunInAff m = RunInAff (m ~> Aff) newtype Runtime props state action m = Runtime - { fresh :: Ref Int + { bindings :: Ref (Map String (Binding state)) + , fresh :: Ref Int , needsStatePublish :: Ref Boolean , props :: Ref props , runInAff :: Ref (RunInAff m) + , runtimeId :: RuntimeId , scope :: Ref (Maybe Scope) , spec :: Ref (RuntimeSpec props state action m) , state :: Ref state - , stateUpdate :: Ref (state -> Effect Unit) + , stateUpdate :: Ref (state -> View state -> Effect Unit) } newtype Scope = Scope { active :: Ref Boolean + , authorities :: Ref (Map String Token) , cleanups :: Ref (Map CleanupId (Effect Unit)) , forks :: Ref (Map ForkId Root) , generation :: Int @@ -168,7 +172,6 @@ newtype Owner = Owner newtype Root = Root { fiber :: Fiber Unit - , onDeactivate :: Effect Unit , owner :: Owner } @@ -191,23 +194,27 @@ createRuntime -> { initialProps :: props , initialState :: state , spec :: RuntimeSpec props state action m - , stateUpdate :: state -> Effect Unit + , stateUpdate :: state -> View state -> Effect Unit } -> Effect (Runtime props state action m) createRuntime runInAff input = do + bindings <- Ref.new Map.empty freshRef <- Ref.new 0 needsStatePublish <- Ref.new false propsRef <- Ref.new input.initialProps runInAffRef <- Ref.new (RunInAff runInAff) + runtimeIdentity <- RuntimeId <$> Ref.new unit scope <- Ref.new Nothing spec <- Ref.new input.spec state <- Ref.new input.initialState stateUpdate <- Ref.new input.stateUpdate pure $ Runtime - { fresh: freshRef + { bindings + , fresh: freshRef , needsStatePublish , props: propsRef , runInAff: runInAffRef + , runtimeId: runtimeIdentity , scope , spec , state @@ -221,7 +228,7 @@ syncSpec . Runtime props state action m -> (m ~> Aff) -> { spec :: RuntimeSpec props state action m - , stateUpdate :: state -> Effect Unit + , stateUpdate :: state -> View state -> Effect Unit } -> Effect Unit syncSpec (Runtime runtime) runInAff input = do @@ -237,11 +244,12 @@ activate runtime@(Runtime state) = do Nothing -> do generation <- fresh runtime active <- Ref.new true + authorities <- Ref.new Map.empty cleanups <- Ref.new Map.empty forks <- Ref.new Map.empty handlers <- Ref.new Map.empty subscriptions <- Ref.new Map.empty - let scope = Scope { active, cleanups, forks, generation, handlers, subscriptions } + let scope = Scope { active, authorities, cleanups, forks, generation, handlers, subscriptions } Ref.write (Just scope) state.scope publishRuntimeState runtime spec <- Ref.read state.spec @@ -265,7 +273,8 @@ deactivate (Runtime state) = do -- Fence every root before normalizing managed state, invoking foreign -- cleanup, or requesting cooperative Aff cancellation. traverse_ fenceRoot roots - traverse_ runRootDeactivation roots + Ref.write Map.empty current.authorities + normalizeRuntimeState (Runtime state) cleanupResults <- traverse Exception.try (Map.values cleanups <> Map.values subscriptions) traverse_ requestCancel roots @@ -342,7 +351,6 @@ fork child = HaloM $ ReaderT \execution -> do { onComplete: do let Scope scope = execution.scope Ref.modify_ (Map.delete fid) scope.forks - , onDeactivate: pure unit , onUnexpected: pure unit } liftEffect do @@ -380,100 +388,153 @@ kill fid = HaloM $ ReaderT \execution -> do root -- Internal managed roots support state-focused lifecycle APIs without exposing --- root identity. Claiming state, fencing prior work, registering the new root, --- and opening its start gate happen in one synchronous runtime transaction. +-- root identity. Slot registration, state reconciliation, authority claim, +-- prior-root fencing, replacement registration, and gate opening form one +-- synchronous runtime transaction. managedStart :: forall props state action m - . Maybe (Aff Unit) - -> ( Int + . Binding state + -> Maybe (Aff Unit) + -> ( RuntimeId + -> Int -> ForkId + -> Maybe Token -> state -> Maybe - { cancel :: Maybe ForkId + { cancel :: Maybe Token , computation :: HaloM props state action m Unit - , onExit :: state -> Maybe state , state :: state + , token :: Token } ) -> HaloM props state action m Unit -managedStart privateDelay claim = HaloM $ ReaderT \execution -> +managedStart binding privateDelay claim = HaloM $ ReaderT \execution -> liftEffect do current <- isCurrent execution when current do - fid <- ForkId <$> fresh execution.runtime - let Scope scope = execution.scope - oldState <- readRuntimeState execution.runtime - case claim scope.generation fid oldState of - Nothing -> pure unit + let + runtime@(Runtime runtimeState) = execution.runtime + Scope scope = execution.scope + brand = Task.bindingBrand binding + oldState <- Ref.read runtimeState.state + registerTaskBinding runtime binding oldState + reconciled <- reconcileExecution execution oldState + displacedRoots <- takeAndFenceTokens execution.scope reconciled.displaced + fid <- ForkId <$> fresh runtime + case + claim runtimeState.runtimeId scope.generation fid + (Map.lookup brand reconciled.authorities) + reconciled.state + of + Nothing -> do + Ref.write reconciled.authorities scope.authorities + when reconciled.changed do + writeAndPublish runtime execution.scope reconciled.state + traverse_ requestCancel displacedRoots Just managed -> do - let - applyExit publish = applyManagedState publish execution.runtime managed.onExit - prepared <- prepare execution.runInAff execution.runtime execution.scope (ForkError fid) + prepared <- prepare execution.runInAff runtime execution.scope (ForkError fid) (withPrivateDelay privateDelay managed.computation) { onComplete: Ref.modify_ (Map.delete fid) scope.forks - , onDeactivate: applyExit false - , onUnexpected: applyExit true + , onUnexpected: exitManaged runtime execution.scope binding managed.token } - previous <- Ref.modify' - ( \forks -> - { state: case managed.cancel of - Nothing -> forks - Just cancelId -> Map.delete cancelId forks - , value: managed.cancel >>= flip Map.lookup forks - } - ) - scope.forks - traverse_ fenceRoot previous - writeRuntimeState execution.runtime managed.state + previousRoots <- takeAndFenceTokens execution.scope case managed.cancel of + Nothing -> [] + Just token -> [ token ] + let authorities = Map.insert brand managed.token reconciled.authorities + Ref.write authorities scope.authorities + Ref.write managed.state runtimeState.state Ref.modify_ (Map.insert fid prepared.root) scope.forks - update <- readStateUpdate execution.runtime - update managed.state - traverse_ requestCancel previous + publishState runtime execution.scope managed.state + traverse_ requestCancel (displacedRoots <> previousRoots) prepared.start +-- Commit a typed managed result only while the canonical focus and runtime +-- authority both contain the exact token. The root remains tracked until its +-- ordinary completion finalizer removes it. +managedComplete + :: forall props state action m + . Binding state + -> Token + -> (state -> Maybe state) + -> HaloM props state action m Unit +managedComplete binding token transition = HaloM $ ReaderT \execution -> + liftEffect do + current <- isCurrent execution + when current do + let + runtime@(Runtime runtimeState) = execution.runtime + Scope scope = execution.scope + brand = Task.bindingBrand binding + authorities <- Ref.read scope.authorities + case Map.lookup brand authorities of + Just authoritative | Task.sameToken token authoritative -> do + oldState <- Ref.read runtimeState.state + case transition oldState of + Nothing -> pure unit + Just newState -> do + Ref.write (Map.delete brand authorities) scope.authorities + Ref.write newState runtimeState.state + publishState runtime execution.scope newState + _ -> pure unit + -- Stop managed work after atomically publishing its replacement state. A root -- that resets itself is cancelled by unwinding its own fiber rather than trying -- to join itself. managedReset :: forall props state action m - . ( Int + . Binding state + -> ( RuntimeId + -> Int + -> Maybe Token -> state -> Maybe - { cancel :: Maybe ForkId + { cancel :: Maybe Token , state :: state } ) -> HaloM props state action m Unit -managedReset transition = HaloM $ ReaderT \execution -> do +managedReset binding transition = HaloM $ ReaderT \execution -> do current <- liftEffect $ isCurrent execution when current do - root <- liftEffect do - let Scope scope = execution.scope - oldState <- readRuntimeState execution.runtime - case transition scope.generation oldState of - Nothing -> pure Nothing + cancellation <- liftEffect do + let + runtime@(Runtime runtimeState) = execution.runtime + Scope scope = execution.scope + brand = Task.bindingBrand binding + oldState <- Ref.read runtimeState.state + registerTaskBinding runtime binding oldState + reconciled <- reconcileExecution execution oldState + displacedRoots <- takeAndFenceTokens execution.scope reconciled.displaced + case + transition runtimeState.runtimeId scope.generation + (Map.lookup brand reconciled.authorities) + reconciled.state + of + Nothing -> do + Ref.write reconciled.authorities scope.authorities + when reconciled.changed do + writeAndPublish runtime execution.scope reconciled.state + traverse_ requestCancel displacedRoots + pure Nothing Just next -> do - previous <- Ref.modify' - ( \forks -> - { state: case next.cancel of - Nothing -> forks - Just cancelId -> Map.delete cancelId forks - , value: next.cancel >>= flip Map.lookup forks - } - ) - scope.forks - traverse_ fenceRoot previous - writeRuntimeState execution.runtime next.state - update <- readStateUpdate execution.runtime - update next.state - pure previous + root <- takeAndFenceTokens execution.scope case next.cancel of + Nothing -> [] + Just token -> [ token ] + let authorities = Map.delete brand reconciled.authorities + Ref.write authorities scope.authorities + Ref.write next.state runtimeState.state + publishState runtime execution.scope next.state + traverse_ requestCancel displacedRoots + pure case root of + [ managedRoot ] -> Just managedRoot + _ -> Nothing traverse_ ( \managedRoot -> if sameOwner execution.owner managedRoot then Aff.throwError scopeCancellationError else cancelRootAff managedRoot ) - root + cancellation -- | Register synchronous `Effect` cleanup in the current activation scope. -- | @@ -574,7 +635,6 @@ startHandler runtime@(Runtime state) scope@(Scope current) context computation = runInAff <- Ref.read state.runInAff prepared <- prepare runInAff runtime scope context computation { onComplete: Ref.modify_ (Map.delete runId) current.handlers - , onDeactivate: pure unit , onUnexpected: pure unit } Ref.modify_ (Map.insert runId prepared.root) current.handlers @@ -588,7 +648,6 @@ prepare -> ErrorContext props action -> HaloM props state action m Unit -> { onComplete :: Effect Unit - , onDeactivate :: Effect Unit , onUnexpected :: Effect Unit } -> Effect Prepared @@ -616,7 +675,7 @@ prepare runInAff runtime scope context computation hooks = do liftEffect $ spec.onError context error Right _ -> pure unit pure - { root: Root { fiber, onDeactivate: hooks.onDeactivate, owner } + { root: Root { fiber, owner } , start: Aff.launchAff_ (AVar.put unit gate) } @@ -637,43 +696,183 @@ withPrivateDelay privateDelay computation = HaloM $ ReaderT \execution -> do traverse_ identity privateDelay runHaloM execution computation -applyManagedState +type ReconciledState state = + { authorities :: Map String Token + , changed :: Boolean + , displaced :: Array Token + , state :: state + } + +registerTaskBinding :: forall props state action m - . Boolean - -> Runtime props state action m - -> (state -> Maybe state) + . Runtime props state action m + -> Binding state + -> state -> Effect Unit -applyManagedState publish (Runtime current) transition = do - oldState <- Ref.read current.state - case transition oldState of - Nothing -> pure unit - Just newState -> do - Ref.write newState current.state - if publish then do - update <- Ref.read current.stateUpdate - update newState - else Ref.write true current.needsStatePublish +registerTaskBinding (Runtime runtime) binding componentState = do + bindings <- Ref.read runtime.bindings + let brand = Task.bindingBrand binding + case Map.lookup brand bindings of + Just existing -> do + sameFocus <- Task.sameBindingFocus componentState existing binding + unless sameFocus $ Exception.throw $ + "Halo task slot \"" <> brand <> "\" is already bound to a different state focus" + Nothing -> do + collision <- foldM + ( \found existing -> case found of + Just _ -> pure found + Nothing -> do + sameFocus <- Task.sameBindingFocus componentState existing binding + pure if sameFocus then Just (Task.bindingBrand existing) else Nothing + ) + Nothing + (Map.values bindings) + case collision of + Just existingBrand -> Exception.throw $ + "Halo task slot \"" <> brand <> "\" overlaps state focus bound as \"" <> existingBrand <> "\"" + Nothing -> Ref.write (Map.insert brand binding bindings) runtime.bindings + +reconcileExecution + :: forall props state action m + . Execution props state action m + -> state + -> Effect (ReconciledState state) +reconcileExecution execution componentState = do + let + Runtime runtime = execution.runtime + Scope scope = execution.scope + bindings <- Ref.read runtime.bindings + authorities <- Ref.read scope.authorities + pure $ reconcileBindings bindings authorities componentState + +reconcileBindings + :: forall state + . Map String (Binding state) + -> Map String Token + -> state + -> ReconciledState state +reconcileBindings bindings authorities componentState = + foldl reconcileOne + { authorities + , changed: false + , displaced: [] + , state: componentState + } + (Map.values bindings) + where + reconcileOne current binding = + let + brand = Task.bindingBrand binding + result = Task.reconcileBinding binding (Map.lookup brand current.authorities) current.state + nextAuthorities = case result.authority of + Nothing -> Map.delete brand current.authorities + Just token -> Map.insert brand token current.authorities + displaced = case result.displaced of + Nothing -> current.displaced + Just token -> current.displaced <> [ token ] + in + { authorities: nextAuthorities + , changed: current.changed || result.changed || case result.displaced of + Nothing -> false + Just _ -> true + , displaced + , state: result.state + } + +commitReconciledState + :: forall props state action m + . Execution props state action m + -> state + -> Effect Unit +commitReconciledState execution proposedState = do + let + runtime@(Runtime runtimeState) = execution.runtime + Scope scope = execution.scope + reconciled <- reconcileExecution execution proposedState + displacedRoots <- takeAndFenceTokens execution.scope reconciled.displaced + Ref.write reconciled.authorities scope.authorities + Ref.write reconciled.state runtimeState.state + publishState runtime execution.scope reconciled.state + traverse_ requestCancel displacedRoots + +normalizeRuntimeState :: forall props state action m. Runtime props state action m -> Effect Unit +normalizeRuntimeState (Runtime runtime) = do + bindings <- Ref.read runtime.bindings + currentState <- Ref.read runtime.state + let reconciled = reconcileBindings bindings Map.empty currentState + when reconciled.changed do + Ref.write reconciled.state runtime.state + Ref.write true runtime.needsStatePublish + +exitManaged + :: forall props state action m + . Runtime props state action m + -> Scope + -> Binding state + -> Token + -> Effect Unit +exitManaged runtime@(Runtime runtimeState) scope@(Scope scopeState) binding token = do + authorities <- Ref.read scopeState.authorities + let brand = Task.bindingBrand binding + case Map.lookup brand authorities of + Just current | Task.sameToken token current -> do + oldState <- Ref.read runtimeState.state + case Task.clearBinding binding token oldState of + Nothing -> pure unit + Just newState -> do + Ref.write (Map.delete brand authorities) scopeState.authorities + Ref.write newState runtimeState.state + publishState runtime scope newState + _ -> pure unit publishRuntimeState :: forall props state action m. Runtime props state action m -> Effect Unit -publishRuntimeState (Runtime runtime) = do - needsPublish <- Ref.read runtime.needsStatePublish +publishRuntimeState runtime@(Runtime runtimeState) = do + needsPublish <- Ref.read runtimeState.needsStatePublish when needsPublish do - Ref.write false runtime.needsStatePublish - currentState <- Ref.read runtime.state - update <- Ref.read runtime.stateUpdate - update currentState - -readRuntimeState :: forall props state action m. Runtime props state action m -> Effect state -readRuntimeState (Runtime runtime) = Ref.read runtime.state - -writeRuntimeState :: forall props state action m. Runtime props state action m -> state -> Effect Unit -writeRuntimeState (Runtime runtime) = flip Ref.write runtime.state + Ref.write false runtimeState.needsStatePublish + currentState <- Ref.read runtimeState.state + activeScope <- Ref.read runtimeState.scope + case activeScope of + Nothing -> pure unit + Just scope -> publishState runtime scope currentState + +writeAndPublish + :: forall props state action m + . Runtime props state action m + -> Scope + -> state + -> Effect Unit +writeAndPublish runtime@(Runtime runtimeState) scope componentState = do + Ref.write componentState runtimeState.state + publishState runtime scope componentState -readStateUpdate +publishState :: forall props state action m . Runtime props state action m - -> Effect (state -> Effect Unit) -readStateUpdate (Runtime runtime) = Ref.read runtime.stateUpdate + -> Scope + -> state + -> Effect Unit +publishState (Runtime runtime) (Scope scope) componentState = do + authorities <- Ref.read scope.authorities + update <- Ref.read runtime.stateUpdate + update componentState (Task.makeView componentState authorities) + +takeAndFenceTokens :: Scope -> Array Token -> Effect (Array Root) +takeAndFenceTokens (Scope scope) = foldM takeOne [] + where + takeOne roots token = do + let fid = Task.tokenForkId token + root <- Ref.modify' + ( \forks -> + { state: Map.delete fid forks + , value: Map.lookup fid forks + } + ) + scope.forks + traverse_ fenceRoot root + pure $ roots <> case root of + Nothing -> [] + Just managedRoot -> [ managedRoot ] createOwner :: Effect Owner createOwner = do @@ -685,9 +884,6 @@ fenceRoot (Root root) = do let Owner owner = root.owner Ref.write false owner.alive -runRootDeactivation :: Root -> Effect Unit -runRootDeactivation (Root root) = root.onDeactivate - sameOwner :: Owner -> Root -> Boolean sameOwner (Owner left) (Root right) = case right.owner of Owner owner -> unsafeRefEq left.alive owner.alive diff --git a/src/React/Halo/Internal/Task.purs b/src/React/Halo/Internal/Task.purs index afb527f..0fba712 100644 --- a/src/React/Halo/Internal/Task.purs +++ b/src/React/Halo/Internal/Task.purs @@ -1,117 +1,21 @@ module React.Halo.Internal.Task - ( State - , Status(..) - , _Active - , _Failed - , _Idle - , _Succeeded - , asStatus - , debounce + ( debounce , debounceWith - , idle - , isActive , once , reset , startIfInactive , supersede - , toMaybe - , toStatus ) where import Prelude -import Control.Monad.State.Class (state) -import Data.Either (Either(..)) -import Data.Lens (ALens', Getter', Prism', prism', to, withLens) -import Data.Maybe (Maybe(..), fromMaybe) -import Data.Tuple (Tuple(..)) +import Data.Either (Either) +import Data.Maybe (Maybe(..)) import Effect.Aff (Aff, Milliseconds(..)) import Effect.Aff as Aff -import React.Halo.Internal.Runtime (HaloM, managedReset, managedStart) -import React.Halo.Internal.Types (ForkId) - --- | Read-only task lifecycle projected from abstract component state. -data Status error result - = Idle - | Active - | Failed error - | Succeeded result - -derive instance eqStatus :: (Eq error, Eq result) => Eq (Status error result) - -instance showStatus :: (Show error, Show result) => Show (Status error result) where - show = case _ of - Idle -> "Idle" - Active -> "Active" - Failed error -> "(Failed " <> show error <> ")" - Succeeded result -> "(Succeeded " <> show result <> ")" - -data Lifecycle error result - = LifecycleIdle - | LifecycleActive Run - | LifecycleFailed error - | LifecycleSucceeded result - -type Run = - { forkId :: ForkId - , generation :: Int - , sequence :: Int - } - --- | Task lifecycle stored inside component state. The constructor is kept --- | private by `React.Halo.Task` because active values carry runtime ownership. -newtype State error result = State - { lifecycle :: Lifecycle error result - , nextSequence :: Int - } - --- | Initial task state. -idle :: forall error result. State error result -idle = State { lifecycle: LifecycleIdle, nextSequence: 0 } - --- | Project abstract task state to its public status. -toStatus :: forall error result. State error result -> Status error result -toStatus (State task) = case task.lifecycle of - LifecycleIdle -> Idle - LifecycleActive _ -> Active - LifecycleFailed error -> Failed error - LifecycleSucceeded result -> Succeeded result - --- | Read-only optic from task state to public status. -asStatus :: forall error result. Getter' (State error result) (Status error result) -asStatus = to toStatus - --- | Return a successful result, if present. -toMaybe :: forall error result. State error result -> Maybe result -toMaybe (State task) = case task.lifecycle of - LifecycleSucceeded result -> Just result - _ -> Nothing - --- | Test whether a debounce timer or task body is active. -isActive :: forall error result. State error result -> Boolean -isActive (State task) = case task.lifecycle of - LifecycleActive _ -> true - _ -> false - -_Idle :: forall error result. Prism' (Status error result) Unit -_Idle = prism' (const Idle) case _ of - Idle -> Just unit - _ -> Nothing - -_Active :: forall error result. Prism' (Status error result) Unit -_Active = prism' (const Active) case _ of - Active -> Just unit - _ -> Nothing - -_Failed :: forall error result. Prism' (Status error result) error -_Failed = prism' Failed case _ of - Failed error -> Just error - _ -> Nothing - -_Succeeded :: forall error result. Prism' (Status error result) result -_Succeeded = prism' Succeeded case _ of - Succeeded result -> Just result - _ -> Nothing +import React.Halo.Internal.Runtime (HaloM, managedComplete, managedReset, managedStart) +import React.Halo.Internal.Task.Types (Slot) +import React.Halo.Internal.Task.Types as Task data Policy = Once @@ -121,16 +25,16 @@ data Policy -- | Start only from `Idle`. Typed failure and success remain terminal until -- | `reset`. once - :: forall props componentState action m error result - . ALens' componentState (State error result) + :: forall name props componentState action m error result + . Slot name componentState error result -> HaloM props componentState action m (Either error result) -> HaloM props componentState action m Unit once = launch Once Nothing -- | Start from `Idle`, `Failed`, or `Succeeded`, but preserve active work. startIfInactive - :: forall props componentState action m error result - . ALens' componentState (State error result) + :: forall name props componentState action m error result + . Slot name componentState error result -> HaloM props componentState action m (Either error result) -> HaloM props componentState action m Unit startIfInactive = launch IfInactive Nothing @@ -138,8 +42,8 @@ startIfInactive = launch IfInactive Nothing -- | Make a new invocation authoritative immediately, fencing and requesting -- | cancellation of prior managed work without waiting for its finalizers. supersede - :: forall props componentState action m error result - . ALens' componentState (State error result) + :: forall name props componentState action m error result + . Slot name componentState error result -> HaloM props componentState action m (Either error result) -> HaloM props componentState action m Unit supersede = launch Supersede Nothing @@ -147,158 +51,81 @@ supersede = launch Supersede Nothing -- | Trailing-edge latest-wins task invocation. The private cancellable timer and -- | the executing body both project to `Active`. debounce - :: forall props componentState action m error result - . ALens' componentState (State error result) + :: forall name props componentState action m error result + . Slot name componentState error result -> Milliseconds -> HaloM props componentState action m (Either error result) -> HaloM props componentState action m Unit debounce = debounceWith Aff.delay --- Internal deterministic scheduler seam used by runtime tests. +-- Internal deterministic timer seam used by runtime tests. debounceWith - :: forall props componentState action m error result + :: forall name props componentState action m error result . (Milliseconds -> Aff Unit) - -> ALens' componentState (State error result) + -> Slot name componentState error result -> Milliseconds -> HaloM props componentState action m (Either error result) -> HaloM props componentState action m Unit debounceWith schedule target duration = launch Supersede (Just (schedule (nonNegative duration))) target --- | Clear terminal state or cancel active work. Active cancellation is fenced --- | synchronously and waits for Aff finalizers before returning. +-- | Clear terminal state or cancel authoritative active work. Cancellation is +-- | fenced synchronously and waits for Aff finalizers before returning. reset - :: forall props componentState action m error result - . ALens' componentState (State error result) + :: forall name props componentState action m error result + . Slot name componentState error result -> HaloM props componentState action m Unit -reset target = withLens target \getTask setTask -> - managedReset \generation componentState -> - let - State task = getTask componentState - nextState = State (task { lifecycle = LifecycleIdle }) - replacement = setTask componentState nextState - in - case task.lifecycle of - LifecycleIdle -> Nothing - LifecycleActive run -> Just - { cancel: if run.generation == generation then Just run.forkId else Nothing - , state: replacement - } - LifecycleFailed _ -> Just { cancel: Nothing, state: replacement } - LifecycleSucceeded _ -> Just { cancel: Nothing, state: replacement } +reset target = managedReset (Task.bindingOf target) \_ _ authority componentState -> + case Task.statusAt target authority componentState of + Task.Idle -> Nothing + Task.Active -> Just + { cancel: authority + , state: Task.idleSlot target componentState + } + Task.Failed _ -> Just + { cancel: Nothing + , state: Task.idleSlot target componentState + } + Task.Succeeded _ -> Just + { cancel: Nothing + , state: Task.idleSlot target componentState + } launch - :: forall props componentState action m error result + :: forall name props componentState action m error result . Policy -> Maybe (Aff Unit) - -> ALens' componentState (State error result) - -> HaloM props componentState action m (Either error result) - -> HaloM props componentState action m Unit -launch policy privateDelay target body = withLens target \getTask setTask -> - managedStart privateDelay \generation forkId componentState -> do - claimed <- claim policy generation forkId (getTask componentState) - pure - { cancel: claimed.cancel - , computation: complete getTask setTask claimed.run body - , onExit: clear getTask setTask claimed.run - , state: setTask componentState claimed.state - } - -claim - :: forall error result - . Policy - -> Int - -> ForkId - -> State error result - -> Maybe - { cancel :: Maybe ForkId - , run :: Run - , state :: State error result - } -claim policy generation forkId (State task) = do - let - currentRun = case task.lifecycle of - LifecycleActive run | run.generation == generation -> Just run - _ -> Nothing - canStart = case policy of - Once -> case task.lifecycle of - LifecycleIdle -> true - LifecycleActive run -> run.generation /= generation - _ -> false - IfInactive -> case currentRun of - Just _ -> false - Nothing -> true - Supersede -> true - if canStart then do - let - run = - { forkId - , generation - , sequence: task.nextSequence - } - nextState = State - { lifecycle: LifecycleActive run - , nextSequence: task.nextSequence + 1 - } - pure - { cancel: case policy of - Supersede -> _.forkId <$> currentRun - _ -> Nothing - , run - , state: nextState - } - else Nothing - -complete - :: forall props componentState action m error result - . (componentState -> State error result) - -> (componentState -> State error result -> componentState) - -> Run + -> Slot name componentState error result -> HaloM props componentState action m (Either error result) -> HaloM props componentState action m Unit -complete getTask setTask run body = do - outcome <- body - state \componentState -> +launch policy privateDelay target body = + managedStart (Task.bindingOf target) privateDelay \runtimeId generation forkId authority componentState -> do let - replacement = updateMatching run outcome (getTask componentState) - in - Tuple unit $ fromMaybe componentState (setTask componentState <$> replacement) - -clear - :: forall componentState error result - . (componentState -> State error result) - -> (componentState -> State error result -> componentState) - -> Run - -> componentState - -> Maybe componentState -clear getTask setTask run componentState = - setTask componentState <$> clearMatching run (getTask componentState) - -updateMatching - :: forall error result - . Run - -> Either error result - -> State error result - -> Maybe (State error result) -updateMatching run outcome (State task) = case task.lifecycle of - LifecycleActive current | sameRun run current -> Just $ State - ( task - { lifecycle = case outcome of - Left error -> LifecycleFailed error - Right result -> LifecycleSucceeded result + status = Task.statusAt target authority componentState + canStart = case policy of + Once -> case status of + Task.Idle -> true + _ -> false + IfInactive -> case status of + Task.Active -> false + _ -> true + Supersede -> true + if canStart then do + let + token = Task.makeToken target runtimeId generation forkId + taskBody = do + outcome <- body + managedComplete (Task.bindingOf target) token + (Task.completeSlot target token outcome) + pure + { cancel: case policy of + Supersede -> authority + _ -> Nothing + , computation: taskBody + , state: Task.activateSlot target token componentState + , token } - ) - _ -> Nothing - -clearMatching :: forall error result. Run -> State error result -> Maybe (State error result) -clearMatching run (State task) = case task.lifecycle of - LifecycleActive current | sameRun run current -> - Just $ State (task { lifecycle = LifecycleIdle }) - _ -> Nothing - -sameRun :: Run -> Run -> Boolean -sameRun left right = - left.generation == right.generation && left.sequence == right.sequence + else Nothing nonNegative :: Milliseconds -> Milliseconds nonNegative (Milliseconds duration) = Milliseconds (max 0.0 duration) diff --git a/src/React/Halo/Internal/Task/Types.purs b/src/React/Halo/Internal/Task/Types.purs new file mode 100644 index 0000000..c289616 --- /dev/null +++ b/src/React/Halo/Internal/Task/Types.purs @@ -0,0 +1,370 @@ +module React.Halo.Internal.Task.Types + ( Binding + , Slot + , State + , Status(..) + , Token + , View + , _Active + , _Failed + , _Idle + , _Succeeded + , activateSlot + , bindingBrand + , bindingOf + , clearBinding + , completeSlot + , emptyView + , idle + , idleSlot + , isActive + , makeToken + , makeView + , reconcileBinding + , sameBindingFocus + , sameToken + , slot + , slotBrand + , statusAt + , toMaybe + , toStatus + , tokenForkId + ) where + +import Prelude + +import Data.Either (Either(..)) +import Data.Lens (ALens', Prism', prism', withLens) +import Data.Map (Map) +import Data.Map as Map +import Data.Maybe (Maybe(..)) +import Data.Symbol (class IsSymbol, reflectSymbol) +import Effect (Effect) +import Effect.Ref (Ref) +import Effect.Ref as Ref +import React.Halo.Internal.Types (ForkId, RuntimeId(..)) +import Type.Proxy (Proxy) +import Unsafe.Reference (unsafeRefEq) + +-- | Public task lifecycle. Runtime ownership remains hidden in `State` and +-- | `View`. +data Status error result + = Idle + | Active + | Failed error + | Succeeded result + +derive instance eqStatus :: (Eq error, Eq result) => Eq (Status error result) + +instance showStatus :: (Show error, Show result) => Show (Status error result) where + show = case _ of + Idle -> "Idle" + Active -> "Active" + Failed error -> "(Failed " <> show error <> ")" + Succeeded result -> "(Succeeded " <> show result <> ")" + +newtype Probe = Probe (Ref Unit) + +newtype Token = Token + { brand :: String + , forkId :: ForkId + , generation :: Int + , runtimeId :: RuntimeId + } + +data Lifecycle error result + = LifecycleIdle + | LifecycleActive Token + | LifecycleFailed error + | LifecycleSucceeded result + | LifecycleProbe Probe + +-- | Freely copyable task lifecycle state. Active authority is validated against +-- | a runtime `View`; the value alone does not own a managed root. +newtype State error result = State + { brand :: String + , lifecycle :: Lifecycle error result + } + +-- | An identity-bearing optic for one task state focus. The name brands values +-- | while the lens locates the canonical component-state field. +data Slot (name :: Symbol) componentState error result = Slot + { binding :: Binding componentState + , brand :: String + , get :: componentState -> State error result + , set :: componentState -> State error result -> componentState + } + +type role Slot nominal representational representational representational + +-- | Immutable task-authority snapshot published with component state. +newtype View state = View + { authorities :: Map String Token + , state :: state + } + +newtype Binding state = Binding + { brand :: String + , clear :: Token -> state -> Maybe state + , mark :: Probe -> state -> state + , reconcile :: + Maybe Token + -> state + -> { authority :: Maybe Token + , changed :: Boolean + , displaced :: Maybe Token + , state :: state + } + , sees :: Probe -> state -> Boolean + } + +-- | Construct a branded task slot from a type-level name and lawful lens. +-- | +-- | On first policy use, a runtime binds the brand to the lens focus. Reusing a +-- | brand at another focus or another brand at the same focus fails in the +-- | calling root's existing error context before mutation or cancellation. +slot + :: forall name componentState error result + . IsSymbol name + => Proxy name + -> ALens' componentState (State error result) + -> Slot name componentState error result +slot proxy target = withLens target \get set -> + let + brand = reflectSymbol proxy + binding = Binding + { brand + , clear: \token componentState -> case get componentState of + State task -> case task.lifecycle of + LifecycleActive current + | task.brand == brand && sameToken token current -> + Just $ set componentState (State { brand, lifecycle: LifecycleIdle }) + _ -> Nothing + , mark: \probe componentState -> + set componentState (State { brand, lifecycle: LifecycleProbe probe }) + , reconcile: reconcileFocus brand get set + , sees: \probe componentState -> case get componentState of + State task -> case task.lifecycle of + LifecycleProbe candidate -> sameProbe probe candidate + _ -> false + } + in + Slot { binding, brand, get, set } + +-- | Construct correctly branded idle state for a slot. +idle :: forall name state error result. Slot name state error result -> State error result +idle (Slot target) = State { brand: target.brand, lifecycle: LifecycleIdle } + +-- | Project one slot through an immutable runtime view. +toStatus + :: forall name state error result + . View state + -> Slot name state error result + -> Status error result +toStatus (View snapshot) target@(Slot slotState) = + statusAt target (Map.lookup slotState.brand snapshot.authorities) snapshot.state + +-- | Return a slot's authoritative successful result, if present. +toMaybe + :: forall name state error result + . View state + -> Slot name state error result + -> Maybe result +toMaybe taskView target = case toStatus taskView target of + Succeeded result -> Just result + _ -> Nothing + +-- | Test whether a slot has an authoritative debounce timer or task body. +isActive + :: forall name state error result + . View state + -> Slot name state error result + -> Boolean +isActive taskView target = case toStatus taskView target of + Active -> true + _ -> false + +_Idle :: forall error result. Prism' (Status error result) Unit +_Idle = prism' (const Idle) case _ of + Idle -> Just unit + _ -> Nothing + +_Active :: forall error result. Prism' (Status error result) Unit +_Active = prism' (const Active) case _ of + Active -> Just unit + _ -> Nothing + +_Failed :: forall error result. Prism' (Status error result) error +_Failed = prism' Failed case _ of + Failed error -> Just error + _ -> Nothing + +_Succeeded :: forall error result. Prism' (Status error result) result +_Succeeded = prism' Succeeded case _ of + Succeeded result -> Just result + _ -> Nothing + +bindingOf :: forall name state error result. Slot name state error result -> Binding state +bindingOf (Slot target) = target.binding + +bindingBrand :: forall state. Binding state -> String +bindingBrand (Binding binding) = binding.brand + +clearBinding :: forall state. Binding state -> Token -> state -> Maybe state +clearBinding (Binding binding) = binding.clear + +slotBrand :: forall name state error result. Slot name state error result -> String +slotBrand (Slot target) = target.brand + +makeToken + :: forall name state error result + . Slot name state error result + -> RuntimeId + -> Int + -> ForkId + -> Token +makeToken (Slot target) runtimeId generation forkId = Token + { brand: target.brand + , forkId + , generation + , runtimeId + } + +tokenForkId :: Token -> ForkId +tokenForkId (Token token) = token.forkId + +sameToken :: Token -> Token -> Boolean +sameToken (Token left) (Token right) = + left.brand == right.brand + && left.generation == right.generation + && left.forkId == right.forkId + && sameRuntime left.runtimeId right.runtimeId + +statusAt + :: forall name state error result + . Slot name state error result + -> Maybe Token + -> state + -> Status error result +statusAt (Slot target) authority componentState = case target.get componentState of + State task + | task.brand /= target.brand -> Idle + | otherwise -> case task.lifecycle of + LifecycleIdle -> Idle + LifecycleActive token -> case authority of + Just current | sameToken token current -> Active + _ -> Idle + LifecycleFailed error -> Failed error + LifecycleSucceeded result -> Succeeded result + LifecycleProbe _ -> Idle + +activateSlot + :: forall name state error result + . Slot name state error result + -> Token + -> state + -> state +activateSlot (Slot target) token componentState = + target.set componentState (State { brand: target.brand, lifecycle: LifecycleActive token }) + +idleSlot + :: forall name state error result + . Slot name state error result + -> state + -> state +idleSlot target@(Slot current) componentState = current.set componentState (idle target) + +completeSlot + :: forall name state error result + . Slot name state error result + -> Token + -> Either error result + -> state + -> Maybe state +completeSlot (Slot target) token outcome componentState = case target.get componentState of + State task -> case task.lifecycle of + LifecycleActive current + | task.brand == target.brand && sameToken token current -> + Just $ target.set componentState $ State + { brand: target.brand + , lifecycle: case outcome of + Left error -> LifecycleFailed error + Right result -> LifecycleSucceeded result + } + _ -> Nothing + +emptyView :: forall state. state -> View state +emptyView state = View { authorities: Map.empty, state } + +makeView :: forall state. state -> Map String Token -> View state +makeView state authorities = View { authorities, state } + +sameBindingFocus :: forall state. state -> Binding state -> Binding state -> Effect Boolean +sameBindingFocus componentState (Binding left) (Binding right) = do + probeRef <- Ref.new unit + let probe = Probe probeRef + pure $ + left.sees probe (right.mark probe componentState) + && right.sees probe (left.mark probe componentState) + +reconcileBinding + :: forall state + . Binding state + -> Maybe Token + -> state + -> { authority :: Maybe Token + , changed :: Boolean + , displaced :: Maybe Token + , state :: state + } +reconcileBinding (Binding binding) = binding.reconcile + +reconcileFocus + :: forall state error result + . String + -> (state -> State error result) + -> (state -> State error result -> state) + -> Maybe Token + -> state + -> { authority :: Maybe Token + , changed :: Boolean + , displaced :: Maybe Token + , state :: state + } +reconcileFocus brand get set authority componentState = + let + State task = get componentState + correctBrand = task.brand == brand + exactActive = case task.lifecycle, authority of + LifecycleActive token, Just current -> correctBrand && sameToken token current + _, _ -> false + validInactive = correctBrand && case task.lifecycle of + LifecycleIdle -> true + LifecycleFailed _ -> true + LifecycleSucceeded _ -> true + _ -> false + in + if exactActive then + { authority + , changed: false + , displaced: Nothing + , state: componentState + } + else if validInactive then + { authority: Nothing + , changed: false + , displaced: authority + , state: componentState + } + else + { authority: Nothing + , changed: true + , displaced: authority + , state: set componentState (State { brand, lifecycle: LifecycleIdle }) + } + +sameProbe :: Probe -> Probe -> Boolean +sameProbe (Probe left) (Probe right) = unsafeRefEq left right + +sameRuntime :: RuntimeId -> RuntimeId -> Boolean +sameRuntime (RuntimeId left) (RuntimeId right) = unsafeRefEq left right diff --git a/src/React/Halo/Internal/Types.purs b/src/React/Halo/Internal/Types.purs index 672eff0..3d6a8f9 100644 --- a/src/React/Halo/Internal/Types.purs +++ b/src/React/Halo/Internal/Types.purs @@ -2,11 +2,14 @@ module React.Halo.Internal.Types ( CleanupId(..) , ErrorContext(..) , ForkId(..) + , RuntimeId(..) , SubscriptionId(..) ) where import Prelude +import Effect.Ref (Ref) + -- | Identifies synchronous cleanup in one React activation. Its constructor is -- | hidden from the root `React.Halo` API. newtype CleanupId = CleanupId Int @@ -30,6 +33,9 @@ data ErrorContext props action | ForkError ForkId | DeactivationError +-- Runtime identity used only by internal ownership tokens. +newtype RuntimeId = RuntimeId (Ref Unit) + -- | Identifies a component-owned process created with `fork`. Its constructor -- | is hidden from the root `React.Halo` API. newtype ForkId = ForkId Int diff --git a/src/React/Halo/Task.purs b/src/React/Halo/Task.purs index b056450..cf41621 100644 --- a/src/React/Halo/Task.purs +++ b/src/React/Halo/Task.purs @@ -1,33 +1,19 @@ -- | Typed lifecycle state for component-owned Halo work. -- | --- | Import this module qualified. A `State error result` is stored inside --- | component state and located with a standard lens; it is not a computation, --- | key, or cache definition. Policy bodies remain ordinary `HaloM` values that --- | return `Either error result`. +-- | Import this module qualified. A branded `Slot` is an identity-bearing +-- | optic for one `State error result` field; it is not a computation, input, +-- | fork handle, or cache definition. Policy bodies remain ordinary `HaloM` +-- | values that return `Either error result`. -- | --- | The mutable representation is abstract because active state carries hidden --- | run ownership. Render through `toStatus`, `asStatus`, `toMaybe`, or --- | `isActive`. Expected `Left` values become `Failed`; unexpected exceptions --- | return the task to `Idle` and follow Halo's normal `ForkError` routing. +-- | `State` is freely copyable, so active authority is validated through the +-- | immutable `View` published with each Halo render. Observe a slot with +-- | `toStatus`, `toMaybe`, or `isActive`. Expected `Left` values become +-- | `Failed`; unexpected exceptions return the task to `Idle` and follow Halo's +-- | normal `ForkError` routing. module React.Halo.Task - ( module Exports + ( module Policies + , module Types ) where -import React.Halo.Internal.Task - ( State - , Status(..) - , _Active - , _Failed - , _Idle - , _Succeeded - , asStatus - , debounce - , idle - , isActive - , once - , reset - , startIfInactive - , supersede - , toMaybe - , toStatus - ) as Exports +import React.Halo.Internal.Task (debounce, once, reset, startIfInactive, supersede) as Policies +import React.Halo.Internal.Task.Types (Slot, State, Status(..), View, _Active, _Failed, _Idle, _Succeeded, idle, isActive, slot, toMaybe, toStatus) as Types diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index cf164e2..eb93fff 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -48,6 +48,9 @@ type State = greetingLens :: Lens' State (Task.State String String) greetingLens = prop (Proxy :: Proxy "greeting") +greetingSlot :: Task.Slot "greeting" State String String +greetingSlot = Task.slot (Proxy :: Proxy "greeting") greetingLens + data Action = Load | Cancel @@ -57,30 +60,30 @@ type UI a = Halo.HaloM Props State Action AppM a handlers :: Halo.Handlers Props State Action AppM handlers = Halo.defaultHandlers { onAction = case _ of - Load -> Task.supersede greetingLens do + Load -> Task.supersede greetingSlot do greeting <- lift loadGreeting pure (Right greeting) - Cancel -> Task.reset greetingLens + Cancel -> Task.reset greetingSlot } loadButton :: Env -> Component Props loadButton env = Halo.component "LoadButton" (runAppM env) - { initialState: \_ -> { greeting: Task.idle } + { initialState: \_ -> { greeting: Task.idle greetingSlot } , handlers , onError: \_ error -> Console.error $ "Unexpected Halo error: " <> message error - , render: \{ props, state, dispatch } -> + , render: \{ props, tasks, dispatch } -> R.div_ [ R.text props.title , R.button { onClick: capture_ (dispatch Load) - , children: [ R.text if Task.isActive state.greeting then "Restart" else "Load" ] + , children: [ R.text if Task.isActive tasks greetingSlot then "Restart" else "Load" ] } , R.button { onClick: capture_ (dispatch Cancel) , children: [ R.text "Cancel" ] } - , R.text $ case Task.toStatus state.greeting of + , R.text $ case Task.toStatus tasks greetingSlot of Task.Idle -> "Not loaded" Task.Active -> "Loading…" Task.Failed error -> error @@ -94,7 +97,7 @@ useExample -> Hook (Halo.UseHalo Props State Action AppM) (Halo.HaloResult State Action) useExample env props = Halo.useHalo (runAppM env) { props - , initialState: { greeting: Task.idle } + , initialState: { greeting: Task.idle greetingSlot } , handlers , onError: \_ _ -> pure unit } diff --git a/test/Test/Halo/GuideExamples.purs b/test/Test/Halo/GuideExamples.purs index 8ca6059..fd44a66 100644 --- a/test/Test/Halo/GuideExamples.purs +++ b/test/Test/Halo/GuideExamples.purs @@ -51,6 +51,9 @@ data SearchAction = Search String | CancelSearch searchLens :: Lens' SearchState (Task.State String Int) searchLens = prop (Proxy :: Proxy "search") +searchSlot :: Task.Slot "search" SearchState String Int +searchSlot = Task.slot (Proxy :: Proxy "search") searchLens + retryingSearch :: String -> Aff (Either String Int) retryingSearch _ = pure (Right 1) @@ -58,13 +61,13 @@ searchHandler :: SearchAction -> Halo.HaloM Unit SearchState SearchAction Aff Unit searchHandler = case _ of - Search query -> Task.debounce searchLens (Milliseconds 250.0) do + Search query -> Task.debounce searchSlot (Milliseconds 250.0) do modify_ _ { query = query } lift (retryingSearch query) - CancelSearch -> Task.reset searchLens + CancelSearch -> Task.reset searchSlot -renderSearch :: SearchState -> String -renderSearch state = case Task.toStatus state.search of +renderSearch :: Task.View SearchState -> String +renderSearch tasks = case Task.toStatus tasks searchSlot of Task.Idle -> "Search" Task.Active -> "Searching" Task.Failed error -> error diff --git a/test/Test/Halo/RuntimeSpec.purs b/test/Test/Halo/RuntimeSpec.purs index 8e71d29..0ed7f67 100644 --- a/test/Test/Halo/RuntimeSpec.purs +++ b/test/Test/Halo/RuntimeSpec.purs @@ -102,7 +102,7 @@ makeRuntime environment = liftEffect do { initialProps: unit , initialState: 0 , spec: { handlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } activate runtime pure { runtime, state } @@ -119,7 +119,7 @@ spec = describe "application monad and parallelism" do liftEffect $ syncSpec runtime (runAppM 2) { spec: { handlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } secondGate <- liftEffect makeGate @@ -145,7 +145,7 @@ spec = describe "application monad and parallelism" do liftEffect $ syncSpec runtime (runAppM 2) { spec: { handlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } release handlerGate diff --git a/test/Test/Halo/ScopeHandlerSpec.purs b/test/Test/Halo/ScopeHandlerSpec.purs index 553b808..503a397 100644 --- a/test/Test/Halo/ScopeHandlerSpec.purs +++ b/test/Test/Halo/ScopeHandlerSpec.purs @@ -51,7 +51,7 @@ spec = describe "scope, handlers, and component-owned forks" do } , onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Unit Int ScopeAction Aff) ) @@ -100,7 +100,7 @@ spec = describe "scope, handlers, and component-owned forks" do } , onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Unit Int ReplayAction Aff) ) @@ -133,7 +133,7 @@ spec = describe "scope, handlers, and component-owned forks" do } , onError: \_ _ -> pure unit } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } :: Effect (Runtime Int Unit Unit Aff) ) @@ -159,7 +159,7 @@ spec = describe "scope, handlers, and component-owned forks" do } , onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Int Int Unit Aff) ) @@ -192,7 +192,7 @@ spec = describe "scope, handlers, and component-owned forks" do } , onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Unit Int ForkAction Aff) ) @@ -237,7 +237,7 @@ spec = describe "scope, handlers, and component-owned forks" do } , onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Unit Int CancelAction Aff) ) @@ -277,7 +277,7 @@ spec = describe "scope, handlers, and component-owned forks" do { initialProps: unit , initialState: 0 , spec: { handlers: oldHandlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Unit Int RefreshAction Aff) ) @@ -286,7 +286,7 @@ spec = describe "scope, handlers, and component-owned forks" do activate runtime syncSpec runtime identityAff { spec: { handlers: newHandlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } dispatch runtime Refresh void $ await "new action handler" newCompleted diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index 5c605ec..7ca9cdb 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -37,7 +37,7 @@ spec = describe "subscriptions, cleanup, and errors" do { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } Aff.finally (liftEffect $ deactivate runtime) do @@ -73,7 +73,7 @@ spec = describe "subscriptions, cleanup, and errors" do Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) errors void $ EffectAVar.tryPut unit releaseFailed } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } liftEffect do @@ -104,7 +104,7 @@ spec = describe "subscriptions, cleanup, and errors" do { handlers: staleCleanupHandlers , onError: \_ _ -> pure unit } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } :: Effect (Runtime Unit Unit StaleCleanupAction Aff) ) @@ -129,7 +129,7 @@ spec = describe "subscriptions, cleanup, and errors" do { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } liftEffect do @@ -159,7 +159,7 @@ spec = describe "subscriptions, cleanup, and errors" do { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } Aff.finally (liftEffect $ deactivate runtime) do @@ -186,7 +186,7 @@ spec = describe "subscriptions, cleanup, and errors" do { initialProps: unit , initialState: Nothing , spec: { handlers: subscriptionHandlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } Aff.finally (liftEffect $ deactivate runtime) do @@ -222,7 +222,7 @@ spec = describe "subscriptions, cleanup, and errors" do { handlers: subscriptionHandlers , onError: onCleanupError oldErrors } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } liftEffect do @@ -246,7 +246,7 @@ spec = describe "subscriptions, cleanup, and errors" do { handlers: subscriptionHandlers , onError: onCleanupError cleanupErrors } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } deactivate runtime void $ await "running action cancellation" gate.settled @@ -280,7 +280,7 @@ spec = describe "subscriptions, cleanup, and errors" do { initialProps: unit , initialState: 0 , spec: { handlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write state + , stateUpdate: \next _ -> Ref.write next state } :: Effect (Runtime Unit Int StaleAction Aff) ) @@ -321,7 +321,7 @@ spec = describe "subscriptions, cleanup, and errors" do Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) newErrors void $ EffectAVar.tryPut unit newRaised } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } release gate void $ await "latest action error callback" newRaised @@ -350,7 +350,7 @@ spec = describe "subscriptions, cleanup, and errors" do Ref.modify_ (_ <> [ label <> ": " <> Exception.message error ]) errors void $ EffectAVar.tryPut unit raised } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } :: Effect (Runtime Unit Unit ErrorAction Aff) ) @@ -433,5 +433,5 @@ makeErrorRuntime errors = createRuntime identityAff { handlers: errorHandlers , onError: \_ error -> Ref.modify_ (_ <> [ Exception.message error ]) errors } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } diff --git a/test/Test/Halo/TaskSpec.purs b/test/Test/Halo/TaskSpec.purs index fa9dd46..bbbbb32 100644 --- a/test/Test/Halo/TaskSpec.purs +++ b/test/Test/Halo/TaskSpec.purs @@ -3,12 +3,13 @@ module Test.Halo.TaskSpec (spec) where import Prelude import Control.Monad.Reader (ReaderT, ask, runReaderT) -import Control.Monad.State (modify_) +import Control.Monad.State (get, modify_, put) import Control.Monad.Trans.Class (lift) import Data.Either (Either(..)) import Data.Lens (Lens', preview, review) import Data.Lens.Record (prop) import Data.Maybe (Maybe(..)) +import Data.Tuple (Tuple(..)) import Effect (Effect) import Effect.Aff (Aff, Milliseconds(..)) import Effect.Aff as Aff @@ -22,6 +23,7 @@ import Effect.Ref as Ref import React.Halo.Handlers (Handlers, defaultHandlers) import React.Halo.Internal.Runtime (HaloM, Runtime, activate, createRuntime, deactivate, dispatch, syncSpec) import React.Halo.Internal.Task as TaskInternal +import React.Halo.Internal.Task.Types as TaskTypes import React.Halo.Internal.Types (ErrorContext(..)) import React.Halo.Task as Task import Test.Halo.Helpers (Gate, await, makeGate, release, shouldNotHaveStarted, waitForGate) @@ -31,12 +33,28 @@ import Type.Proxy (Proxy(..)) type ComponentState = { elsewhere :: Int + , other :: Task.State String Int , task :: Task.State String Int } taskLens :: Lens' ComponentState (Task.State String Int) taskLens = prop (Proxy :: Proxy "task") +taskSlot :: Task.Slot "task" ComponentState String Int +taskSlot = Task.slot (Proxy :: Proxy "task") taskLens + +otherLens :: Lens' ComponentState (Task.State String Int) +otherLens = prop (Proxy :: Proxy "other") + +otherSlot :: Task.Slot "other" ComponentState String Int +otherSlot = Task.slot (Proxy :: Proxy "other") otherLens + +sameBrandOtherSlot :: Task.Slot "task" ComponentState String Int +sameBrandOtherSlot = Task.slot (Proxy :: Proxy "task") otherLens + +differentBrandTaskSlot :: Task.Slot "alias" ComponentState String Int +differentBrandTaskSlot = Task.slot (Proxy :: Proxy "alias") taskLens + data Body = WaitBody Gate (Either String Int) | UpdateBody Gate Int (Either String Int) @@ -54,7 +72,15 @@ data Action | RunStartTwice Body Body (AVar Unit) | RunSupersede Body (AVar Unit) | RunDebounce Timer Milliseconds Body (AVar Unit) + | RunOther Body (AVar Unit) | Reset (AVar Unit) + | ResetOther (AVar Unit) + | AssignIdleAndStart Body (AVar Unit) + | CopyTaskToOther (AVar Unit) + | CaptureState (AVar ComponentState) + | RestoreState ComponentState (AVar Unit) + | RunSameBrandCollision Body + | RunDifferentBrandCollision Body type UI a = HaloM Unit ComponentState Action Aff a @@ -62,24 +88,43 @@ handlers :: Handlers Unit ComponentState Action Aff handlers = defaultHandlers { onAction = case _ of RunOnce body launched -> do - Task.once taskLens (runBody body) + Task.once taskSlot (runBody body) signal launched RunStartIfInactive body launched -> do - Task.startIfInactive taskLens (runBody body) + Task.startIfInactive taskSlot (runBody body) signal launched RunStartTwice first second launched -> do - Task.startIfInactive taskLens (runBody first) - Task.startIfInactive taskLens (runBody second) + Task.startIfInactive taskSlot (runBody first) + Task.startIfInactive taskSlot (runBody second) signal launched RunSupersede body launched -> do - Task.supersede taskLens (runBody body) + Task.supersede taskSlot (runBody body) signal launched RunDebounce timer duration body launched -> do - TaskInternal.debounceWith (runTimer timer) taskLens duration (runBody body) + TaskInternal.debounceWith (runTimer timer) taskSlot duration (runBody body) + signal launched + RunOther body launched -> do + Task.supersede otherSlot (runBody body) signal launched Reset completed -> do - Task.reset taskLens + Task.reset taskSlot signal completed + ResetOther completed -> do + Task.reset otherSlot + signal completed + AssignIdleAndStart body launched -> do + modify_ _ { task = Task.idle taskSlot } + Task.startIfInactive taskSlot (runBody body) + signal launched + CopyTaskToOther completed -> do + modify_ \state -> state { other = state.task } + signal completed + CaptureState captured -> get >>= liftAff <<< void <<< flip AVar.tryPut captured + RestoreState snapshot completed -> do + put snapshot + signal completed + RunSameBrandCollision body -> Task.supersede sameBrandOtherSlot (runBody body) + RunDifferentBrandCollision body -> Task.supersede differentBrandTaskSlot (runBody body) } runBody :: Body -> UI (Either String Int) @@ -111,9 +156,11 @@ signal :: AVar Unit -> UI Unit signal completed = liftAff $ void $ AVar.tryPut unit completed type Harness = - { runtime :: Runtime Unit ComponentState Action Aff + { otherUpdates :: AVar (Task.Status String Int) + , runtime :: Runtime Unit ComponentState Action Aff , setterCalls :: Ref.Ref Int , state :: Ref.Ref ComponentState + , tasks :: Ref.Ref (Task.View ComponentState) , updates :: AVar (Task.Status String Int) } @@ -122,35 +169,49 @@ makeHarness -> Aff Harness makeHarness onError = liftEffect do state <- Ref.new initialState + tasks <- Ref.new (TaskTypes.emptyView initialState) updates <- EffectAVar.empty + otherUpdates <- EffectAVar.empty setterCalls <- Ref.new 0 runtime <- createRuntime identity { initialProps: unit , initialState , spec: { handlers, onError } - , stateUpdate: updateState state updates setterCalls + , stateUpdate: updateState state tasks updates otherUpdates setterCalls } activate runtime - pure { runtime, setterCalls, state, updates } + pure { otherUpdates, runtime, setterCalls, state, tasks, updates } initialState :: ComponentState -initialState = { elsewhere: 0, task: Task.idle } +initialState = + { elsewhere: 0 + , other: Task.idle otherSlot + , task: Task.idle taskSlot + } updateState :: Ref.Ref ComponentState + -> Ref.Ref (Task.View ComponentState) + -> AVar (Task.Status String Int) -> AVar (Task.Status String Int) -> Ref.Ref Int -> ComponentState + -> Task.View ComponentState -> Effect Unit -updateState state updates setterCalls next = do - previous <- Ref.read state +updateState state tasks updates otherUpdates setterCalls next nextTasks = do + previousTasks <- Ref.read tasks Ref.write next state + Ref.write nextTasks tasks Ref.modify_ (_ + 1) setterCalls let - previousStatus = Task.toStatus previous.task - nextStatus = Task.toStatus next.task + previousStatus = Task.toStatus previousTasks taskSlot + nextStatus = Task.toStatus nextTasks taskSlot + previousOtherStatus = Task.toStatus previousTasks otherSlot + nextOtherStatus = Task.toStatus nextTasks otherSlot when (previousStatus /= nextStatus) do void $ EffectAVar.tryPut nextStatus updates + when (previousOtherStatus /= nextOtherStatus) do + void $ EffectAVar.tryPut nextOtherStatus otherUpdates awaitStatus :: String @@ -169,15 +230,18 @@ makeTimer = do pure { duration, gate } statusOf :: Harness -> Effect (Task.Status String Int) -statusOf harness = Task.toStatus <<< _.task <$> Ref.read harness.state +statusOf harness = flip Task.toStatus taskSlot <$> Ref.read harness.tasks + +otherStatusOf :: Harness -> Effect (Task.Status String Int) +otherStatusOf harness = flip Task.toStatus otherSlot <$> Ref.read harness.tasks spec :: Spec Unit spec = describe "state-focused tasks" do it "projects status through helpers and lawful prisms" do - Task.toStatus (Task.idle :: Task.State String Int) `shouldEqual` Task.Idle - Task.toMaybe (Task.idle :: Task.State String Int) `shouldEqual` Nothing - Task.isActive (Task.idle :: Task.State String Int) `shouldEqual` false - preview (Task.asStatus <<< Task._Idle) (Task.idle :: Task.State String Int) `shouldEqual` Just unit + let tasks = TaskTypes.emptyView initialState + Task.toStatus tasks taskSlot `shouldEqual` Task.Idle + Task.toMaybe tasks taskSlot `shouldEqual` Nothing + Task.isActive tasks taskSlot `shouldEqual` false preview Task._Idle (Task.Idle :: Task.Status String Int) `shouldEqual` Just unit preview Task._Active (Task.Active :: Task.Status String Int) `shouldEqual` Just unit preview Task._Failed (Task.Failed "no" :: Task.Status String Int) `shouldEqual` Just "no" @@ -305,7 +369,215 @@ spec = describe "state-focused tasks" do externalAfterRelease `shouldEqual` false stateAfterRelease <- liftEffect $ Ref.read harness.state stateAfterRelease.elsewhere `shouldEqual` 0 - Task.toStatus stateAfterRelease.task `shouldEqual` Task.Succeeded 2 + statusAfterRelease <- liftEffect $ statusOf harness + statusAfterRelease `shouldEqual` Task.Succeeded 2 + + it "reconciles assigned idle before restart and fences the detached root" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + oldWork <- liftEffect makeGate + oldFinalizer <- liftEffect makeGate + newWork <- liftEffect makeGate + externalWitness <- liftEffect $ Ref.new false + oldLaunched <- liftEffect EffectAVar.empty + newLaunched <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunSupersede (CancellableBody oldWork oldFinalizer externalWitness) oldLaunched) + void $ await "assigned-idle old body" oldWork.started + awaitStatus "assigned-idle old active" Task.Active harness.updates + + liftEffect $ dispatch harness.runtime + (AssignIdleAndStart (WaitBody newWork (Right 11)) newLaunched) + void $ await "assigned-idle restart" newLaunched + awaitStatus "assigned idle publication" Task.Idle harness.updates + void $ await "assigned-idle old finalizer" oldFinalizer.started + void $ await "assigned-idle new body" newWork.started + restartedStatus <- liftEffect $ statusOf harness + restartedStatus `shouldEqual` Task.Active + release newWork + awaitStatus "assigned-idle new success" (Task.Succeeded 11) harness.updates + + release oldFinalizer + void $ await "assigned-idle old settlement" oldFinalizer.settled + witness <- liftEffect $ Ref.read externalWitness + witness `shouldEqual` false + state <- liftEffect $ Ref.read harness.state + state.elsewhere `shouldEqual` 0 + status <- liftEffect $ statusOf harness + status `shouldEqual` Task.Succeeded 11 + + it "normalizes a restored stale snapshot and cancels the displaced authority" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + first <- liftEffect makeGate + currentWork <- liftEffect makeGate + currentFinalizer <- liftEffect makeGate + externalWitness <- liftEffect $ Ref.new false + launched <- liftEffect EffectAVar.empty + captured <- liftEffect EffectAVar.empty + restored <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime (RunSupersede (WaitBody first (Right 1)) launched) + void $ await "snapshot first body" first.started + awaitStatus "snapshot first active" Task.Active harness.updates + liftEffect $ dispatch harness.runtime (CaptureState captured) + snapshot <- await "active whole-state snapshot" captured + + liftEffect $ dispatch harness.runtime + (RunSupersede (CancellableBody currentWork currentFinalizer externalWitness) launched) + void $ await "snapshot current body" currentWork.started + void $ await "snapshot old cancellation" first.settled + liftEffect $ dispatch harness.runtime (RestoreState snapshot restored) + void $ await "stale snapshot restoration" restored + void $ await "restored snapshot displaced finalizer" currentFinalizer.started + awaitStatus "restored snapshot idle" Task.Idle harness.updates + + release currentFinalizer + void $ await "displaced current settlement" currentFinalizer.settled + witness <- liftEffect $ Ref.read externalWitness + witness `shouldEqual` false + state <- liftEffect $ Ref.read harness.state + state.elsewhere `shouldEqual` 0 + status <- liftEffect $ statusOf harness + status `shouldEqual` Task.Idle + + it "preserves restored branded terminal state while fencing current work" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + completed <- liftEffect makeGate + currentWork <- liftEffect makeGate + currentFinalizer <- liftEffect makeGate + externalWitness <- liftEffect $ Ref.new false + launched <- liftEffect EffectAVar.empty + captured <- liftEffect EffectAVar.empty + restored <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime (RunSupersede (WaitBody completed (Right 4)) launched) + void $ await "terminal snapshot body" completed.started + awaitStatus "terminal snapshot active" Task.Active harness.updates + release completed + awaitStatus "terminal snapshot success" (Task.Succeeded 4) harness.updates + liftEffect $ dispatch harness.runtime (CaptureState captured) + terminalSnapshot <- await "terminal whole-state snapshot" captured + + liftEffect $ dispatch harness.runtime + (RunSupersede (CancellableBody currentWork currentFinalizer externalWitness) launched) + void $ await "terminal replacement current body" currentWork.started + awaitStatus "terminal replacement active" Task.Active harness.updates + liftEffect $ dispatch harness.runtime (RestoreState terminalSnapshot restored) + void $ await "terminal snapshot restoration" restored + void $ await "terminal replacement finalizer" currentFinalizer.started + awaitStatus "restored terminal status" (Task.Succeeded 4) harness.updates + + release currentFinalizer + void $ await "terminal replacement settlement" currentFinalizer.settled + witness <- liftEffect $ Ref.read externalWitness + witness `shouldEqual` false + state <- liftEffect $ Ref.read harness.state + state.elsewhere `shouldEqual` 0 + + it "keeps same-typed slots isolated when active state is copied" do + harness <- makeHarness \_ _ -> pure unit + Aff.finally (liftEffect $ deactivate harness.runtime) do + taskWork <- liftEffect makeGate + otherWork <- liftEffect makeGate + otherAgain <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + copied <- liftEffect EffectAVar.empty + resetDone <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime (RunSupersede (WaitBody taskWork (Right 1)) launched) + void $ await "source slot body" taskWork.started + awaitStatus "source slot active" Task.Active harness.updates + liftEffect $ dispatch harness.runtime (CopyTaskToOther copied) + void $ await "copy active into unused slot" copied + copiedView <- liftEffect $ Ref.read harness.tasks + Task.toStatus copiedView taskSlot `shouldEqual` Task.Active + Task.toStatus copiedView otherSlot `shouldEqual` Task.Idle + + liftEffect $ dispatch harness.runtime (ResetOther resetDone) + void $ await "reset copied inactive slot" resetDone + sourceSettled <- liftEffect $ EffectAVar.tryTake taskWork.settled + sourceSettled `shouldEqual` Nothing + + liftEffect $ dispatch harness.runtime (RunOther (WaitBody otherWork (Right 2)) launched) + void $ await "independent other slot" otherWork.started + awaitStatus "other slot active update" Task.Active harness.otherUpdates + sourceWhileOther <- liftEffect $ statusOf harness + otherWhileRunning <- liftEffect $ otherStatusOf harness + sourceWhileOther `shouldEqual` Task.Active + otherWhileRunning `shouldEqual` Task.Active + release otherWork + awaitStatus "other slot success" (Task.Succeeded 2) harness.otherUpdates + + liftEffect $ dispatch harness.runtime (CopyTaskToOther copied) + void $ await "copy active after other completion" copied + awaitStatus "copied completed slot becomes idle" Task.Idle harness.otherUpdates + sourceAfterCopy <- liftEffect $ statusOf harness + sourceAfterCopy `shouldEqual` Task.Active + + liftEffect $ dispatch harness.runtime (RunOther (WaitBody otherAgain (Right 3)) launched) + void $ await "other restart after copied state" otherAgain.started + awaitStatus "other restart active" Task.Active harness.otherUpdates + liftEffect $ dispatch harness.runtime (ResetOther resetDone) + void $ await "other reset after restart" resetDone + awaitStatus "other reset idle" Task.Idle harness.otherUpdates + sourceAfterOtherReset <- liftEffect $ statusOf harness + sourceAfterOtherReset `shouldEqual` Task.Active + sourceStillRunning <- liftEffect $ EffectAVar.tryTake taskWork.settled + sourceStillRunning `shouldEqual` Nothing + + release taskWork + awaitStatus "source slot completion" (Task.Succeeded 1) harness.updates + + it "rejects slot identity collisions before mutation or cancellation" do + errors <- liftEffect $ Ref.new [] + raised <- liftEffect EffectAVar.empty + harness <- makeHarness \context error -> do + let + prefix = case context of + ActionError _ -> "action: " + _ -> "wrong: " + Ref.modify_ (_ <> [ prefix <> Exception.message error ]) errors + void $ EffectAVar.tryPut unit raised + Aff.finally (liftEffect $ deactivate harness.runtime) do + authoritative <- liftEffect makeGate + sameBrandBody <- liftEffect makeGate + differentBrandBody <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + + liftEffect $ dispatch harness.runtime + (RunSupersede (WaitBody authoritative (Right 1)) launched) + void $ await "collision authoritative body" authoritative.started + awaitStatus "collision authoritative active" Task.Active harness.updates + callsBefore <- liftEffect $ Ref.read harness.setterCalls + + liftEffect $ dispatch harness.runtime + (RunSameBrandCollision (WaitBody sameBrandBody (Right 2))) + void $ await "same-brand collision" raised + shouldNotHaveStarted sameBrandBody + firstStillRunning <- liftEffect $ EffectAVar.tryTake authoritative.settled + firstStillRunning `shouldEqual` Nothing + + liftEffect $ dispatch harness.runtime + (RunDifferentBrandCollision (WaitBody differentBrandBody (Right 3))) + void $ await "different-brand collision" raised + shouldNotHaveStarted differentBrandBody + secondStillRunning <- liftEffect $ EffectAVar.tryTake authoritative.settled + secondStillRunning `shouldEqual` Nothing + status <- liftEffect $ statusOf harness + status `shouldEqual` Task.Active + callsAfter <- liftEffect $ Ref.read harness.setterCalls + callsAfter `shouldEqual` callsBefore + + actual <- liftEffect $ Ref.read errors + actual `shouldEqual` + [ "action: Halo task slot \"task\" is already bound to a different state focus" + , "action: Halo task slot \"alias\" overlaps state focus bound as \"task\"" + ] + release authoritative + awaitStatus "collision authority completion" (Task.Succeeded 1) harness.updates it "reset publishes Idle immediately and waits for finalizers" do harness <- makeHarness \_ _ -> pure unit @@ -409,24 +681,100 @@ spec = describe "state-focused tasks" do awaitStatus "debounce reset idle" Task.Idle harness.updates shouldNotHaveStarted body + it "publishes two-slot normalization before StrictMode onActivate work" do + firstTask <- liftEffect makeGate + firstOther <- liftEffect makeGate + nextTask <- liftEffect makeGate + nextOther <- liftEffect makeGate + taskGate <- liftEffect $ Ref.new firstTask + otherGate <- liftEffect $ Ref.new firstOther + snapshots <- liftEffect $ Ref.new [] + setterCalls <- liftEffect $ Ref.new 0 + let + activationHandlers = defaultHandlers + { onActivate = do + currentTask <- liftEffect $ Ref.read taskGate + currentOther <- liftEffect $ Ref.read otherGate + Task.once taskSlot do + liftAff $ waitForGate currentTask + pure (Right 1) + Task.once otherSlot do + liftAff $ waitForGate currentOther + pure (Right 2) + } + stateUpdate _ taskView = do + Ref.modify_ (_ + 1) setterCalls + Ref.modify_ + ( _ <> + [ Tuple + (Task.toStatus taskView taskSlot) + (Task.toStatus taskView otherSlot) + ] + ) + snapshots + runtime <- liftEffect $ createRuntime identity + { initialProps: unit + , initialState + , spec: { handlers: activationHandlers, onError: \_ _ -> pure unit } + , stateUpdate + } + + liftEffect $ activate runtime + void $ await "first StrictMode task" firstTask.started + void $ await "first StrictMode other" firstOther.started + beforeCleanup <- liftEffect $ Ref.read snapshots + beforeCleanup `shouldEqual` + [ Tuple Task.Active Task.Idle + , Tuple Task.Active Task.Active + ] + callsBeforeCleanup <- liftEffect $ Ref.read setterCalls + + liftEffect $ deactivate runtime + void $ await "first StrictMode task cancellation" firstTask.settled + void $ await "first StrictMode other cancellation" firstOther.settled + callsAfterCleanup <- liftEffect $ Ref.read setterCalls + callsAfterCleanup `shouldEqual` callsBeforeCleanup + + liftEffect do + Ref.write nextTask taskGate + Ref.write nextOther otherGate + activate runtime + void $ await "replayed StrictMode task" nextTask.started + void $ await "replayed StrictMode other" nextOther.started + afterReplay <- liftEffect $ Ref.read snapshots + afterReplay `shouldEqual` + [ Tuple Task.Active Task.Idle + , Tuple Task.Active Task.Active + , Tuple Task.Idle Task.Idle + , Tuple Task.Active Task.Idle + , Tuple Task.Active Task.Active + ] + liftEffect $ deactivate runtime + it "normalizes active state without a cleanup setter and republishes before reactivation work" do harness <- makeHarness \_ _ -> pure unit first <- liftEffect makeGate + other <- liftEffect makeGate second <- liftEffect makeGate launched <- liftEffect EffectAVar.empty liftEffect $ dispatch harness.runtime (RunOnce (WaitBody first (Right 1)) launched) void $ await "pre-deactivation task" first.started awaitStatus "pre-deactivation active" Task.Active harness.updates + liftEffect $ dispatch harness.runtime (RunOther (WaitBody other (Right 9)) launched) + void $ await "pre-deactivation other task" other.started + awaitStatus "pre-deactivation other active" Task.Active harness.otherUpdates callsBefore <- liftEffect $ Ref.read harness.setterCalls liftEffect $ deactivate harness.runtime void $ await "deactivated task cancellation" first.settled + void $ await "deactivated other cancellation" other.settled callsAfterCleanup <- liftEffect $ Ref.read harness.setterCalls callsAfterCleanup `shouldEqual` callsBefore liftEffect $ activate harness.runtime awaitStatus "reactivation idle publication" Task.Idle harness.updates + awaitStatus "reactivation other idle publication" Task.Idle harness.otherUpdates liftEffect $ dispatch harness.runtime (RunOnce (WaitBody second (Right 2)) launched) void $ await "reactivated once task" second.started awaitStatus "reactivated task active" Task.Active harness.updates @@ -443,26 +791,68 @@ spec = describe "state-focused tasks" do terminal `shouldEqual` Task.Succeeded 2 liftEffect $ deactivate harness.runtime + it "projects a cross-runtime active snapshot Idle on first render" do + source <- makeHarness \_ _ -> pure unit + sourceWork <- liftEffect makeGate + launched <- liftEffect EffectAVar.empty + liftEffect $ dispatch source.runtime (RunSupersede (WaitBody sourceWork (Right 1)) launched) + void $ await "cross-runtime source body" sourceWork.started + awaitStatus "cross-runtime source active" Task.Active source.updates + foreignState <- liftEffect $ Ref.read source.state + + let firstView = TaskTypes.emptyView foreignState + Task.toStatus firstView taskSlot `shouldEqual` Task.Idle + targetState <- liftEffect $ Ref.new foreignState + targetTasks <- liftEffect $ Ref.new firstView + target <- liftEffect $ createRuntime identity + { initialProps: unit + , initialState: foreignState + , spec: { handlers, onError: \_ _ -> pure unit } + , stateUpdate: \state tasks -> do + Ref.write state targetState + Ref.write tasks targetTasks + } + targetWork <- liftEffect makeGate + Aff.finally + ( liftEffect do + deactivate target + deactivate source.runtime + ) + do + liftEffect do + activate target + dispatch target (RunOnce (WaitBody targetWork (Right 2)) launched) + void $ await "cross-runtime target body" targetWork.started + sourceSettled <- liftEffect $ EffectAVar.tryTake sourceWork.settled + sourceSettled `shouldEqual` Nothing + currentTargetTasks <- liftEffect $ Ref.read targetTasks + Task.toStatus currentTargetTasks taskSlot `shouldEqual` Task.Active + release targetWork + release sourceWork + it "uses the latest state setter for managed completion" do harness <- makeHarness \_ _ -> pure unit Aff.finally (liftEffect $ deactivate harness.runtime) do gate <- liftEffect makeGate launched <- liftEffect EffectAVar.empty newState <- liftEffect $ Ref.new initialState + newTasks <- liftEffect $ Ref.new (TaskTypes.emptyView initialState) liftEffect $ dispatch harness.runtime (RunSupersede (WaitBody gate (Right 7)) launched) void $ await "setter task body" gate.started awaitStatus "setter task active" Task.Active harness.updates liftEffect $ syncSpec harness.runtime identity { spec: { handlers, onError: \_ _ -> pure unit } - , stateUpdate: flip Ref.write newState + , stateUpdate: \next tasks -> do + Ref.write next newState + Ref.write tasks newTasks } release gate void $ await "setter task settlement" gate.settled - current <- liftEffect $ Ref.read newState - Task.toStatus current.task `shouldEqual` Task.Succeeded 7 - old <- liftEffect $ Ref.read harness.state - Task.toStatus old.task `shouldEqual` Task.Active + currentTasks <- liftEffect $ Ref.read newTasks + Task.toStatus currentTasks taskSlot `shouldEqual` Task.Succeeded 7 + oldTasks <- liftEffect $ Ref.read harness.tasks + Task.toStatus oldTasks taskSlot `shouldEqual` Task.Active snapshotSpec @@ -485,13 +875,16 @@ type SnapshotState = { task :: Task.State String Int } snapshotLens :: Lens' SnapshotState (Task.State String Int) snapshotLens = prop (Proxy :: Proxy "task") +snapshotSlot :: Task.Slot "snapshot" SnapshotState String Int +snapshotSlot = Task.slot (Proxy :: Proxy "snapshot") snapshotLens + data SnapshotAction = LaunchSnapshot Gate Gate (AVar Int) snapshotHandlers :: Handlers Unit SnapshotState SnapshotAction AppM snapshotHandlers = defaultHandlers { onAction = \(LaunchSnapshot handlerGate bodyGate result) -> do lift $ AppM $ lift $ waitForGate handlerGate - Task.supersede snapshotLens do + Task.supersede snapshotSlot do lift $ AppM $ lift $ waitForGate bodyGate environment <- lift readEnvironment lift $ AppM $ lift $ void $ AVar.tryPut environment result @@ -506,9 +899,9 @@ snapshotSpec = describe "managed task interpreter snapshots" do result <- liftEffect EffectAVar.empty runtime <- liftEffect $ createRuntime (runAppM 1) { initialProps: unit - , initialState: { task: Task.idle } + , initialState: { task: Task.idle snapshotSlot } , spec: { handlers: snapshotHandlers, onError: \_ _ -> pure unit } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } Aff.finally (liftEffect $ deactivate runtime) do liftEffect do @@ -517,7 +910,7 @@ snapshotSpec = describe "managed task interpreter snapshots" do void $ await "snapshot handler" handlerGate.started liftEffect $ syncSpec runtime (runAppM 2) { spec: { handlers: snapshotHandlers, onError: \_ _ -> pure unit } - , stateUpdate: \_ -> pure unit + , stateUpdate: \_ _ -> pure unit } release handlerGate void $ await "snapshot task body" bodyGate.started From abcdfadb7f33d486ccb0faf1a25ff5c47c539d24 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Wed, 2 Sep 2026 01:59:50 +0900 Subject: [PATCH 13/16] Fix documented consumer dependencies --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 729c4d0..e18ee8a 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,6 @@ package: - effect - either - exceptions - - foldable-traversable - - maybe - prelude - profunctor-lenses - react-basic-dom @@ -46,7 +44,7 @@ workspace: After v4 is published, the local override can be replaced with: ```console -spago install aff console effect either exceptions foldable-traversable maybe prelude profunctor-lenses react-basic-dom react-basic-hooks react-halo transformers +spago install aff console effect either exceptions prelude profunctor-lenses react-basic-dom react-basic-hooks react-halo transformers ``` `react-basic-dom` is used by this example, not required by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. From 43333971925b04af7de84fc0ffec3d8249083967 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Wed, 2 Sep 2026 02:08:26 +0900 Subject: [PATCH 14/16] Derive task slots from record labels --- README.md | 14 ++++---------- docs/guide.md | 11 ++++------- src/React/Halo/Internal/Task/Types.purs | 19 ++++++++++++++++--- src/React/Halo/Task.purs | 2 +- test/Test/Halo/DocExamples.purs | 7 +------ test/Test/Halo/GuideExamples.purs | 7 +------ test/Test/Halo/TaskSpec.purs | 15 ++++++--------- 7 files changed, 33 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index e18ee8a..277ed20 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ package: - either - exceptions - prelude - - profunctor-lenses - react-basic-dom - react-basic-hooks - react-halo @@ -44,7 +43,7 @@ workspace: After v4 is published, the local override can be replaced with: ```console -spago install aff console effect either exceptions prelude profunctor-lenses react-basic-dom react-basic-hooks react-halo transformers +spago install aff console effect either exceptions prelude react-basic-dom react-basic-hooks react-halo transformers ``` `react-basic-dom` is used by this example, not required by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. @@ -75,11 +74,9 @@ loadGreeting = AppM do liftAff env.loadGreeting ``` -Define component state and an action ADT. Import `React.Halo.Task` qualified and locate its abstract state with a standard lens: +Define component state and an action ADT. Import `React.Halo.Task` qualified and bind each abstract task field by its record label: ```purescript -import Data.Lens (Lens') -import Data.Lens.Record (prop) import React.Halo.Task as Task import Type.Proxy (Proxy(..)) @@ -89,11 +86,8 @@ type State = { greeting :: Task.State String String } -greetingLens :: Lens' State (Task.State String String) -greetingLens = prop (Proxy :: Proxy "greeting") - greetingSlot :: Task.Slot "greeting" State String String -greetingSlot = Task.slot (Proxy :: Proxy "greeting") greetingLens +greetingSlot = Task.slot (Proxy :: Proxy "greeting") data Action = Load | Cancel @@ -110,7 +104,7 @@ handlers = Halo.defaultHandlers } ``` -A slot is an opaque identity-bearing optic for one task field. The type-level name distinguishes same-typed fields; it does not store a body, input, or cancellation key. A task body is ordinary `HaloM` and returns `Either error result`. `supersede` makes the new invocation authoritative immediately; `reset` cancels active work and waits for its Aff finalizers. +A slot is an opaque identity-bearing optic for one task field. `Task.slot` uses the type-level name as both the record label and task identity, so the common case needs no separate lens. `Task.slotAt` accepts a custom lawful lens for a nested focus. A slot does not store a body, input, or cancellation key. A task body is ordinary `HaloM` and returns `Either error result`. `supersede` makes the new invocation authoritative immediately; `reset` cancels active work and waits for its Aff finalizers. `lift` is `Control.Monad.Trans.Class.lift`. Each handler captures the interpreter current when it starts. Managed tasks and forks inherit their launching handler's interpreter, even if React renders with a newer interpreter before their bodies begin. diff --git a/docs/guide.md b/docs/guide.md index 7521385..40c9580 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -96,11 +96,9 @@ Capture props before asynchronous work when that work must use one render's valu ## Store typed task outcomes in component state -Import `React.Halo.Task` qualified when component state should retain the lifecycle and typed result of owned work. Pair each `Task.State error result` field with an opaque branded slot built from a type-level name and standard lens: +Import `React.Halo.Task` qualified when component state should retain the lifecycle and typed result of owned work. Pair each `Task.State error result` field with an opaque branded slot built from its type-level record label: ```purescript -import Data.Lens (Lens') -import Data.Lens.Record (prop) import React.Halo.Task as Task import Type.Proxy (Proxy(..)) @@ -109,11 +107,8 @@ type State = , query :: String } -searchLens :: Lens' State (Task.State SearchError Results) -searchLens = prop (Proxy :: Proxy "search") - searchSlot :: Task.Slot "search" State SearchError Results -searchSlot = Task.slot (Proxy :: Proxy "search") searchLens +searchSlot = Task.slot (Proxy :: Proxy "search") initialState = { search: Task.idle searchSlot @@ -121,6 +116,8 @@ initialState = } ``` +`Task.slot` derives the record-field lens from the same label used for identity, so ordinary record fields need no separate lens declaration. Use `Task.slotAt` with a custom lawful lens only for a nested focus. + `Task.State` remains ordinary, freely copyable component data; it cannot by itself prove ownership of a live root. `Task.View` validates the canonical slot against an immutable runtime authority snapshot. Copying active state to another slot, restoring stale state, or supplying state from another runtime therefore projects `Idle` and cannot cross-cancel authoritative work. A slot name must identify exactly one state focus for the runtime lifetime, and one focus cannot use multiple names. Halo validates the lawful lenses on first use. A collision fails before state mutation or cancellation and follows the current handler or fork error context. diff --git a/src/React/Halo/Internal/Task/Types.purs b/src/React/Halo/Internal/Task/Types.purs index c289616..70bb012 100644 --- a/src/React/Halo/Internal/Task/Types.purs +++ b/src/React/Halo/Internal/Task/Types.purs @@ -24,6 +24,7 @@ module React.Halo.Internal.Task.Types , sameBindingFocus , sameToken , slot + , slotAt , slotBrand , statusAt , toMaybe @@ -35,6 +36,7 @@ import Prelude import Data.Either (Either(..)) import Data.Lens (ALens', Prism', prism', withLens) +import Data.Lens.Record (prop) import Data.Map (Map) import Data.Map as Map import Data.Maybe (Maybe(..)) @@ -42,6 +44,7 @@ import Data.Symbol (class IsSymbol, reflectSymbol) import Effect (Effect) import Effect.Ref (Ref) import Effect.Ref as Ref +import Prim.Row as Row import React.Halo.Internal.Types (ForkId, RuntimeId(..)) import Type.Proxy (Proxy) import Unsafe.Reference (unsafeRefEq) @@ -118,18 +121,28 @@ newtype Binding state = Binding , sees :: Probe -> state -> Boolean } --- | Construct a branded task slot from a type-level name and lawful lens. +-- | Construct a task slot for the record field named by the type-level label. +-- | The same label supplies both the slot brand and its lawful record lens. +slot + :: forall name row componentRow error result + . IsSymbol name + => Row.Cons name (State error result) row componentRow + => Proxy name + -> Slot name (Record componentRow) error result +slot proxy = slotAt proxy (prop proxy) + +-- | Construct a branded task slot for a nested or custom lawful focus. -- | -- | On first policy use, a runtime binds the brand to the lens focus. Reusing a -- | brand at another focus or another brand at the same focus fails in the -- | calling root's existing error context before mutation or cancellation. -slot +slotAt :: forall name componentState error result . IsSymbol name => Proxy name -> ALens' componentState (State error result) -> Slot name componentState error result -slot proxy target = withLens target \get set -> +slotAt proxy target = withLens target \get set -> let brand = reflectSymbol proxy binding = Binding diff --git a/src/React/Halo/Task.purs b/src/React/Halo/Task.purs index cf41621..5f997d5 100644 --- a/src/React/Halo/Task.purs +++ b/src/React/Halo/Task.purs @@ -16,4 +16,4 @@ module React.Halo.Task ) where import React.Halo.Internal.Task (debounce, once, reset, startIfInactive, supersede) as Policies -import React.Halo.Internal.Task.Types (Slot, State, Status(..), View, _Active, _Failed, _Idle, _Succeeded, idle, isActive, slot, toMaybe, toStatus) as Types +import React.Halo.Internal.Task.Types (Slot, State, Status(..), View, _Active, _Failed, _Idle, _Succeeded, idle, isActive, slot, slotAt, toMaybe, toStatus) as Types diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index eb93fff..e75de37 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -5,8 +5,6 @@ import Prelude import Control.Monad.Reader (ReaderT, ask, runReaderT) import Control.Monad.Trans.Class (lift) import Data.Either (Either(..)) -import Data.Lens (Lens') -import Data.Lens.Record (prop) import Effect.Aff (Aff) import Effect.Aff.Class (class MonadAff, liftAff) import Effect.Class (class MonadEffect) @@ -45,11 +43,8 @@ type State = { greeting :: Task.State String String } -greetingLens :: Lens' State (Task.State String String) -greetingLens = prop (Proxy :: Proxy "greeting") - greetingSlot :: Task.Slot "greeting" State String String -greetingSlot = Task.slot (Proxy :: Proxy "greeting") greetingLens +greetingSlot = Task.slot (Proxy :: Proxy "greeting") data Action = Load diff --git a/test/Test/Halo/GuideExamples.purs b/test/Test/Halo/GuideExamples.purs index fd44a66..10228ff 100644 --- a/test/Test/Halo/GuideExamples.purs +++ b/test/Test/Halo/GuideExamples.purs @@ -6,8 +6,6 @@ import Control.Monad.State (modify_) import Control.Monad.Trans.Class (lift) import Control.Parallel (parallel, sequential) import Data.Either (Either(..)) -import Data.Lens (Lens') -import Data.Lens.Record (prop) import Data.Tuple (Tuple(..)) import Effect.Aff (Aff, Milliseconds(..)) import React.Halo as Halo @@ -48,11 +46,8 @@ type SearchState = data SearchAction = Search String | CancelSearch -searchLens :: Lens' SearchState (Task.State String Int) -searchLens = prop (Proxy :: Proxy "search") - searchSlot :: Task.Slot "search" SearchState String Int -searchSlot = Task.slot (Proxy :: Proxy "search") searchLens +searchSlot = Task.slot (Proxy :: Proxy "search") retryingSearch :: String -> Aff (Either String Int) retryingSearch _ = pure (Right 1) diff --git a/test/Test/Halo/TaskSpec.purs b/test/Test/Halo/TaskSpec.purs index bbbbb32..beac339 100644 --- a/test/Test/Halo/TaskSpec.purs +++ b/test/Test/Halo/TaskSpec.purs @@ -41,19 +41,19 @@ taskLens :: Lens' ComponentState (Task.State String Int) taskLens = prop (Proxy :: Proxy "task") taskSlot :: Task.Slot "task" ComponentState String Int -taskSlot = Task.slot (Proxy :: Proxy "task") taskLens +taskSlot = Task.slot (Proxy :: Proxy "task") otherLens :: Lens' ComponentState (Task.State String Int) otherLens = prop (Proxy :: Proxy "other") otherSlot :: Task.Slot "other" ComponentState String Int -otherSlot = Task.slot (Proxy :: Proxy "other") otherLens +otherSlot = Task.slot (Proxy :: Proxy "other") sameBrandOtherSlot :: Task.Slot "task" ComponentState String Int -sameBrandOtherSlot = Task.slot (Proxy :: Proxy "task") otherLens +sameBrandOtherSlot = Task.slotAt (Proxy :: Proxy "task") otherLens differentBrandTaskSlot :: Task.Slot "alias" ComponentState String Int -differentBrandTaskSlot = Task.slot (Proxy :: Proxy "alias") taskLens +differentBrandTaskSlot = Task.slotAt (Proxy :: Proxy "alias") taskLens data Body = WaitBody Gate (Either String Int) @@ -872,11 +872,8 @@ readEnvironment = AppM ask type SnapshotState = { task :: Task.State String Int } -snapshotLens :: Lens' SnapshotState (Task.State String Int) -snapshotLens = prop (Proxy :: Proxy "task") - -snapshotSlot :: Task.Slot "snapshot" SnapshotState String Int -snapshotSlot = Task.slot (Proxy :: Proxy "snapshot") snapshotLens +snapshotSlot :: Task.Slot "task" SnapshotState String Int +snapshotSlot = Task.slot (Proxy :: Proxy "task") data SnapshotAction = LaunchSnapshot Gate Gate (AVar Int) From 85db4c7857b7539e2a5599613b72d5a28e0554b8 Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Wed, 2 Sep 2026 03:02:31 +0900 Subject: [PATCH 15/16] Add Functor instance for emitters --- src/React/Halo/Subscription.purs | 8 +++++++- test/Test/Halo/SubscriptionErrorSpec.purs | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/React/Halo/Subscription.purs b/src/React/Halo/Subscription.purs index f812589..c4d3907 100644 --- a/src/React/Halo/Subscription.purs +++ b/src/React/Halo/Subscription.purs @@ -4,7 +4,7 @@ module React.Halo.Subscription , runEmitter ) where -import Prelude (Unit) +import Prelude (Unit, class Functor, (<<<)) import Effect (Effect) @@ -18,6 +18,12 @@ import Effect (Effect) newtype Emitter action = Emitter ((action -> Effect Unit) -> Effect (Effect Unit)) +-- | Transform each emitted value before it reaches the receiver. Mapping keeps +-- | the source's registration and cleanup behavior unchanged. +instance functorEmitter :: Functor Emitter where + map transform (Emitter register) = Emitter \receive -> + register (receive <<< transform) + -- | Create an emitter from registration logic. During deactivation, a throwing -- | cleanup is isolated from the remaining scope cleanup and reported as -- | `DeactivationError`. diff --git a/test/Test/Halo/SubscriptionErrorSpec.purs b/test/Test/Halo/SubscriptionErrorSpec.purs index 7ca9cdb..2cc9546 100644 --- a/test/Test/Halo/SubscriptionErrorSpec.purs +++ b/test/Test/Halo/SubscriptionErrorSpec.purs @@ -18,7 +18,7 @@ import Effect.Ref as Ref import React.Halo.Handlers (Handlers, defaultHandlers) import React.Halo.Internal.Runtime (Runtime, activate, createRuntime, deactivate, dispatch, fork, registerCleanup, releaseCleanup, subscribe, syncSpec, unsubscribe) import React.Halo.Internal.Types (CleanupId(..), ErrorContext(..), ForkId, SubscriptionId) -import React.Halo.Subscription (Emitter, makeEmitter) +import React.Halo.Subscription (Emitter, makeEmitter, runEmitter) import Test.Halo.Helpers (Gate, await, makeGate, release, waitForGate) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) @@ -28,6 +28,23 @@ identityAff = identity spec :: Spec Unit spec = describe "subscriptions, cleanup, and errors" do + it "maps emitted values while preserving source cleanup" do + received <- liftEffect $ Ref.new [] + cleanupCount <- liftEffect $ Ref.new 0 + let + numbers = makeEmitter \receive -> do + receive 1 + receive 2 + pure $ Ref.modify_ (_ + 1) cleanupCount + actions = (_ + 10) <$> numbers + cleanup <- liftEffect $ runEmitter actions \value -> + Ref.modify_ (_ <> [ value ]) received + liftEffect cleanup + actual <- liftEffect $ Ref.read received + actual `shouldEqual` [ 11, 12 ] + cleaned <- liftEffect $ Ref.read cleanupCount + cleaned `shouldEqual` 1 + it "releases generic cleanup once and ignores unknown IDs" do cleanupCount <- liftEffect $ Ref.new 0 registered <- liftEffect EffectAVar.empty From a87f4a5da236c5d51fc5efbbab444958c14d0ebc Mon Sep 17 00:00:00 2001 From: Robert Porter Date: Wed, 2 Sep 2026 03:02:54 +0900 Subject: [PATCH 16/16] Organize onboarding by progressive concepts --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 240 +++++++++++---------- docs/guide.md | 296 +++----------------------- docs/guide/actions-and-state.md | 111 ++++++++++ docs/guide/getting-started.md | 143 +++++++++++++ docs/guide/lifecycle-and-resources.md | 136 ++++++++++++ docs/guide/managed-work.md | 158 ++++++++++++++ test/Test/Halo/DocExamples.purs | 61 +++--- test/Test/Halo/GuideExamples.purs | 135 ++++++++++-- 10 files changed, 851 insertions(+), 433 deletions(-) create mode 100644 docs/guide/actions-and-state.md create mode 100644 docs/guide/getting-started.md create mode 100644 docs/guide/lifecycle-and-resources.md create mode 100644 docs/guide/managed-work.md diff --git a/AGENTS.md b/AGENTS.md index 1dace2b..8502441 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Use this file as the repository control plane. Do not treat it as a substitute f For an unfamiliar task, route yourself by scope: - Read [README.md](README.md) for the product boundary and current public mental model. -- Read [docs/guide.md](docs/guide.md) for supported usage and cancellation guidance. +- Use the [guide index](docs/guide.md) to select the relevant public contract: [actions and state](docs/guide/actions-and-state.md), [managed work](docs/guide/managed-work.md), or [lifecycle and resources](docs/guide/lifecycle-and-resources.md). - Read [docs/architecture.md](docs/architecture.md) before changing runtime ownership, interpreters, concurrency, subscriptions, or error handling. - Read [CONTRIBUTING.md](CONTRIBUTING.md) for setup, validation, and pull request readiness. - Inspect [`React.Halo`](src/React/Halo.purs) and the public module that owns an API before changing its contract. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b8a285..d9ca349 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -This guide covers local setup, validation, and pull request readiness. Start with the [README](README.md) for the public mental model, the [guide](docs/guide.md) for API usage, and the [architecture notes](docs/architecture.md) before changing runtime ownership or cancellation behavior. +This guide covers local setup, validation, and pull request readiness. Start with the [README](README.md) for the public mental model, use the [guide index](docs/guide.md) to find the relevant usage chapter, and read the [architecture notes](docs/architecture.md) before changing runtime ownership or cancellation behavior. ## Set up the checkout diff --git a/README.md b/README.md index 277ed20..e866839 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,177 @@ # React Halo -Halo gives a PureScript React component a typed action handler, local state, and a safe boundary for application effects. Your application logic remains in its own monad; Halo adds access to props and state, action dispatch, component-owned processes, subscriptions, and cleanup. +Halo gives a PureScript React component typed actions, local state, and a safe boundary for application effects. Application logic remains in its own monad; Halo owns the UI work started by a React component. -Use Halo when several UI interactions share state and asynchronous work must remain owned by the component. For a single request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. +Use Halo when several interactions share state or asynchronous work must be cancelled with the component. For one request derived directly from render dependencies, `React.Basic.Hooks.Aff.useAff` is usually simpler. -## How Halo fits +## Choose a React entry point -A Halo component has three main parts: +Use `component` when Halo owns the complete component: -1. **Actions** describe UI interactions. Rendering code calls `dispatch :: action -> Effect Unit`, and Halo starts the corresponding action handler in the active component scope. -2. **Application effects** remain in an application monad such as `ReaderT Env Aff`. Standard `lift` embeds those effects in `HaloM`, and an interpreter supplied at the React boundary translates them to `Aff`. -3. **Forks** are cancellable processes owned by the active component. A fork may outlive the handler that started it, but it cannot outlive the React activation that owns it. +```purescript +profileComponent env = + Halo.component "Profile" (runAppM env) + { initialState + , handlers + , onError + , render + } +``` -Halo does not provide global state, server caching, or a separate process runtime. +Use `useHalo` when the render function also uses other hooks: -## Install this unreleased version +```purescript +halo <- Halo.useHalo (runAppM env) + { props + , initialState + , handlers + , onError + } -The API documented on this branch is not published yet; the PureScript Registry currently resolves `react-halo` to v3. This branch uses the PureScript and Spago versions pinned in [`package.json`](package.json). +-- halo.state +-- halo.tasks +-- halo.dispatch +``` -Add a checkout as a local Spago package and declare the dependencies imported by the example below: +Both entry points receive an interpreter from the application's monad to `Aff`. They expose current component state, an immutable task view, and synchronous action dispatch. `component` also passes current props to its renderer. -```yaml -package: - dependencies: - - aff - - console - - effect - - either - - exceptions - - prelude - - react-basic-dom - - react-basic-hooks - - react-halo - - transformers - -workspace: - extraPackages: - react-halo: - path: ../purescript-react-halo -``` +## Understand the core model -After v4 is published, the local override can be replaced with: +Actions describe UI events, handlers perform component work, and rendering dispatches the next action: -```console -spago install aff console effect either exceptions prelude react-basic-dom react-basic-hooks react-halo transformers -``` +```purescript +data Action + = Rename String + | LoadProfile -`react-basic-dom` is used by this example, not required by Halo itself. Your application also needs the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. +handlers = Halo.defaultHandlers + { onAction = case _ of + Rename name -> + modify_ _ { name = name } -## Quick start + LoadProfile -> do + profile <- lift Profile.load + modify_ _ { profile = Just profile } + } +``` -Define an application monad and the interpreter that runs it: +`HaloM props state action m` owns component state and props while preserving application capabilities in `m`: ```purescript -type Env = { loadGreeting :: Aff String } +type UI a = Halo.HaloM Props State Action AppM a newtype AppM a = AppM (ReaderT Env Aff a) -derive newtype instance functorAppM :: Functor AppM -derive newtype instance applyAppM :: Apply AppM -derive newtype instance applicativeAppM :: Applicative AppM -derive newtype instance bindAppM :: Bind AppM -derive newtype instance monadAppM :: Monad AppM -derive newtype instance monadEffectAppM :: MonadEffect AppM -derive newtype instance monadAffAppM :: MonadAff AppM - runAppM :: Env -> AppM ~> Aff runAppM env (AppM program) = runReaderT program env - -loadGreeting :: AppM String -loadGreeting = AppM do - env <- ask - liftAff env.loadGreeting ``` -Define component state and an action ADT. Import `React.Halo.Task` qualified and bind each abstract task field by its record label: +Standard transformer `lift` crosses that boundary. Each dispatched action starts an independent handler, so long-running handlers can overlap. React deactivation fences every handler before requesting cancellation. + +Rendering reads state and dispatches actions synchronously: ```purescript -import React.Halo.Task as Task -import Type.Proxy (Proxy(..)) +render { state, dispatch } = + R.button + { onClick: capture_ (dispatch LoadProfile) + , children: [ R.text state.name ] + } +``` + +## Choose the ownership mechanism -type Props = { title :: String } +Start work directly in an action handler. Use a stronger mechanism only when the interaction needs it. +| Need | Use | +|---|---| +| Handle one UI event | action handler | +| Retain typed idle, active, failure, and success state | managed task | +| Let work outlive its launching handler | component-owned fork | +| Receive actions from an external event source | emitter subscription | +| Release another synchronous resource | registered cleanup | + +A task stores a typed outcome in component state and renders through the matching task view: + +```purescript type State = - { greeting :: Task.State String String + { search :: Task.State SearchError Results } -greetingSlot :: Task.Slot "greeting" State String String -greetingSlot = Task.slot (Proxy :: Proxy "greeting") +searchSlot :: Task.Slot "search" State SearchError Results +searchSlot = Task.slot (Proxy :: Proxy "search") -data Action = Load | Cancel +Search query -> Task.supersede searchSlot do + lift (Search.run query) -type UI a = Halo.HaloM Props State Action AppM a +case Task.toStatus tasks searchSlot of + Task.Idle -> renderPrompt + Task.Active -> renderSpinner + Task.Failed error -> renderError error + Task.Succeeded results -> renderResults results +``` -handlers :: Halo.Handlers Props State Action AppM -handlers = Halo.defaultHandlers - { onAction = case _ of - Load -> Task.supersede greetingSlot do - greeting <- lift loadGreeting - pure (Right greeting) +`Task.slot` uses one type-level label as both record field and identity. Use `Task.slotAt` only for a nested or custom lawful focus. Task bodies remain ordinary `HaloM` values returning `Either error result`. - Cancel -> Task.reset greetingSlot - } +A fork is an independently cancellable component process: + +```purescript +fiber <- Halo.fork synchronize +Halo.kill fiber ``` -A slot is an opaque identity-bearing optic for one task field. `Task.slot` uses the type-level name as both the record label and task identity, so the common case needs no separate lens. `Task.slotAt` accepts a custom lawful lens for a nested focus. A slot does not store a body, input, or cancellation key. A task body is ordinary `HaloM` and returns `Either error result`. `supersede` makes the new invocation authoritative immediately; `reset` cancels active work and waits for its Aff finalizers. +Subscriptions turn external callbacks into actions: -`lift` is `Control.Monad.Trans.Class.lift`. Each handler captures the interpreter current when it starts. Managed tasks and forks inherit their launching handler's interpreter, even if React renders with a newer interpreter before their bodies begin. +```purescript +names = Halo.makeEmitter \emit -> source.listen emit +actions = NameChanged <$> names +void $ Halo.subscribe actions +``` -Supply the interpreter when creating the component: +Other synchronous resources can register cleanup directly: ```purescript -loadButton :: Env -> Component Props -loadButton env = Halo.component "LoadButton" (runAppM env) - { initialState: \_ -> - { greeting: Task.idle greetingSlot } - , handlers - , onError: \_ error -> - Console.error $ "Unexpected Halo error: " <> message error - , render: \{ props, tasks, dispatch } -> - R.div_ - [ R.text props.title - , R.button - { onClick: capture_ (dispatch Load) - , children: [ R.text if Task.isActive tasks greetingSlot then "Restart" else "Load" ] - } - , R.button - { onClick: capture_ (dispatch Cancel) - , children: [ R.text "Cancel" ] - } - , R.text $ case Task.toStatus tasks greetingSlot of - Task.Idle -> "Not loaded" - Task.Active -> "Loading…" - Task.Failed error -> error - Task.Succeeded greeting -> greeting - ] - } +cleanupId <- Halo.registerCleanup removeListener +Halo.releaseCleanup cleanupId ``` -`state` and `tasks` come from one coherent render snapshot. `Task.State` values can be copied as ordinary component data, but only the canonical slot with matching runtime authority projects `Active`; stale, foreign, or cross-slot active values project `Idle`. +React cleanup is synchronous. Put asynchronous release in an Aff finalizer owned by a handler, task, or fork rather than an `onDeactivate` callback. -`initialState` receives the initial props once per mount. Later prop changes call `handlers.onPropsChange`; they do not recreate state. +## Install this unreleased version -Use the hook form when Halo shares a component with other hooks: +The API on this branch is not published yet; the PureScript Registry currently resolves `react-halo` to v3. This branch uses the PureScript and Spago versions pinned in [`package.json`](package.json). -```purescript -halo <- Halo.useHalo (runAppM env) - { props - , initialState - , handlers - , onError - } +Add a checkout as a local Spago package and declare the dependencies used by your application: --- halo.state --- halo.tasks --- halo.dispatch +```yaml +package: + dependencies: + - aff + - console + - effect + - either + - exceptions + - prelude + - react-basic-dom + - react-basic-hooks + - react-halo + - transformers + +workspace: + extraPackages: + react-halo: + path: ../purescript-react-halo ``` -A complete version of this example is compiled as [`test/Test/Halo/DocExamples.purs`](test/Test/Halo/DocExamples.purs). +After v4 is published, replace the local override with a registry installation: + +```console +spago install aff console effect either exceptions prelude react-basic-dom react-basic-hooks react-halo transformers +``` -For a synchronous resource that is not an emitter subscription, use `Halo.registerCleanup cleanup`. Halo runs every still-registered `Effect Unit` when the React activation deactivates. `Halo.releaseCleanup id` removes and runs one cleanup immediately; it is not an asynchronous deactivation callback. +`react-basic-dom` is used by the examples, not required by Halo itself. Applications also need the JavaScript packages required by `react-basic-hooks`, including React. Halo has no npm runtime entry point or npm runtime dependencies. -## Learn more +## Documentation -- The [Halo guide](docs/guide.md) explains tasks, component processes, cleanup, cancellation, parallelism, subscriptions, and errors. -- Generate the exact API reference from public source comments with `npx spago docs --offline`. -- The [runtime architecture](docs/architecture.md) describes ownership and cancellation invariants for maintainers. -- See [Contributing](CONTRIBUTING.md) before changing the library. +The [guide](docs/guide.md) covers complete usage and ownership choices. Generate exact API documentation from public source comments with `npx spago docs --offline`. Maintainers changing runtime ownership should also read the [architecture notes](docs/architecture.md) and [contributor guide](CONTRIBUTING.md). -The deterministic tests model React's setup-cleanup-setup sequence directly. The repository does not yet include a real DOM/StrictMode mounting fixture. +Halo does not provide global state, server caching, backpressure queues, or a detached scheduler. The deterministic suite models React setup-cleanup-setup at the runtime boundary; the repository does not yet contain a real DOM/StrictMode mounting fixture. diff --git a/docs/guide.md b/docs/guide.md index 40c9580..5ea3be2 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,284 +1,48 @@ # Halo guide -Halo combines a typed UI action handler with component state and a runtime boundary for your application monad. Start with the [README quick start](../README.md), then use this guide when choosing lifetimes, cancellation, or cleanup behavior. +Halo connects a React entry point to typed actions, component state, and application effects. Begin with the entry point your component needs, then add stronger ownership only when an interaction requires it. -## Run application logic through `AppM` +## Start here -An application monad commonly carries services or configuration over `Aff`: +- Use [`Halo.component`](guide/getting-started.md#use-component-for-a-complete-component) when Halo owns the complete component. +- Use [`Halo.useHalo`](guide/getting-started.md#use-usehalo-with-other-hooks) when the render function also uses other hooks. -```purescript -newtype AppM a = AppM (ReaderT AppEnv Aff a) +The [getting-started chapter](guide/getting-started.md) builds both forms from the React boundary inward: initial state, rendering, dispatch, handlers, AppM, and error reporting. -runAppM :: AppEnv -> AppM ~> Aff -runAppM env (AppM program) = runReaderT program env -``` - -Use it as the fourth `HaloM` parameter: - -```purescript -type UI a = HaloM Props State Action AppM a -``` - -Standard transformer `lift` runs application logic inside a Halo computation: - -```purescript -import Control.Monad.Trans.Class (lift) - -loadAccount :: UI Unit -loadAccount = do - account <- lift Account.load - modify_ _ { account = Just account } -``` - -Supply the interpreter at the React boundary: - -```purescript -Halo.component "Account" (runAppM env) spec -Halo.useHalo (runAppM env) hookSpec -``` - -Each new handler captures the latest interpreter supplied to the hook. A fork inherits the interpreter captured by the root that launches it. This keeps one running action on one application environment even when a later render supplies another interpreter. - -The interpreter must return the `Aff` that performs the work. Do not detach it with `launchAff_`; Halo can own and cancel only the returned computation. - -## Handle a UI action ADT - -Rendering code receives `dispatch :: action -> Effect Unit`. Each dispatch starts `handlers.onAction action` in the current component scope: - -```purescript -data Action - = NameChanged String - | Save - | CancelSave - -handlers = Halo.defaultHandlers - { onAction = case _ of - NameChanged name -> modify_ _ { name = name } - Save -> save - CancelSave -> cancelSave - } -``` - -Action handlers overlap. A long-running action does not block a later dispatch, and React deactivation cancels every handler still running in that activation. - -When a process must outlive its handler or another action must cancel it, start a component-owned fork instead of leaving the work in the handler. - -## Update state without stale snapshots - -`HaloM` has `MonadState state`. Use normal `get`, `put`, `gets`, `modify`, and `modify_` operations. - -State operations run against the state current at that operation. Avoid reading a whole state value, waiting for an application effect, and then writing a modified copy of the old value: - -```purescript --- Avoid: another action can update state while save runs. -old <- get -result <- lift (save old.form) -put (old { result = Just result }) -``` - -Capture only the input needed by the effect, then update the current state after it completes: - -```purescript -form <- gets _.form -result <- lift (save form) -modify_ _ { result = Just result } -``` - -`Halo.getProps` reads the latest props. `onPropsChange` receives the previous props, so both sides of a synchronization are available: - -```purescript -onPropsChange = \previous -> do - current <- Halo.getProps - synchronize previous current -``` - -Capture props before asynchronous work when that work must use one render's value. Otherwise, a later `getProps` intentionally returns newer props. - -## Store typed task outcomes in component state - -Import `React.Halo.Task` qualified when component state should retain the lifecycle and typed result of owned work. Pair each `Task.State error result` field with an opaque branded slot built from its type-level record label: - -```purescript -import React.Halo.Task as Task -import Type.Proxy (Proxy(..)) - -type State = - { search :: Task.State SearchError Results - , query :: String - } - -searchSlot :: Task.Slot "search" State SearchError Results -searchSlot = Task.slot (Proxy :: Proxy "search") - -initialState = - { search: Task.idle searchSlot - , query: "" - } -``` - -`Task.slot` derives the record-field lens from the same label used for identity, so ordinary record fields need no separate lens declaration. Use `Task.slotAt` with a custom lawful lens only for a nested focus. - -`Task.State` remains ordinary, freely copyable component data; it cannot by itself prove ownership of a live root. `Task.View` validates the canonical slot against an immutable runtime authority snapshot. Copying active state to another slot, restoring stale state, or supplying state from another runtime therefore projects `Idle` and cannot cross-cancel authoritative work. - -A slot name must identify exactly one state focus for the runtime lifetime, and one focus cannot use multiple names. Halo validates the lawful lenses on first use. A collision fails before state mutation or cancellation and follows the current handler or fork error context. - -A policy body remains ordinary `HaloM` and returns `Either error result`. It may update other component state. Halo atomically stores a matching `Left` as `Failed` or `Right` as `Succeeded`: - -```purescript -Search query -> Task.supersede searchSlot do - modify_ _ { query = query } - lift (Search.run query) - -CancelSearch -> Task.reset searchSlot -``` +## Choose the next chapter -Choose a policy by invocation semantics: +### [Actions, effects, and state](guide/actions-and-state.md) -- `once slot body` starts only from `Idle`; success and typed failure remain terminal until `reset`. -- `startIfInactive slot body` ignores a call while active, but starts from `Idle`, `Failed`, or `Succeeded`. -- `supersede slot body` makes every new call authoritative immediately. Prior work is fenced and cancellation is requested without waiting, so its finalizers may overlap the new body but cannot commit Halo state or begin another lifted application effect. -- `debounce slot milliseconds body` is trailing-edge latest-wins. Its private timer and body both render as `Active`; a new call cancels either phase. Nonpositive durations use a scheduled zero delay. -- `reset slot` publishes `Idle`, cancels active work, and waits for its Aff finalizers. Terminal state is cleared immediately. +Use this chapter to answer: -Render through the read-only projection: +- How does `dispatch` reach `onAction`? +- How do I run AppM logic with `lift`? +- How do overlapping handlers affect state? +- How do I read current props or run independent effects in parallel? -```purescript -case Task.toStatus tasks searchSlot of - Task.Idle -> renderPrompt - Task.Active -> renderSpinner - Task.Failed error -> renderError error - Task.Succeeded results -> renderResults results -``` - -A component renderer receives `tasks :: Task.View State` beside `state`; `useHalo` returns the same view. The state and view are one coherent immutable React snapshot. `Task.toMaybe tasks slot` returns only a succeeded result, and `Task.isActive tasks slot` covers both the private debounce timer and executing body. `_Idle`, `_Active`, `_Failed`, and `_Succeeded` remain lawful prisms over `Task.Status`. - -Expected failures belong in `Either`. An unexpected exception returns the matching task to `Idle` and is reported through the latest `onError` as `ForkError`. Cancellation is neither a typed failure nor an unexpected error. Put retry policy in AppM and lift the already-retrying computation; when nested under `debounce`, the debounce timer runs once and AppM then owns its attempts. A retry loop must let Aff cancellation propagate rather than catching every exception. - -Task state is component-owned result storage, not a global cache. Calls do not retain an input or computation for later reruns. - -## Start and kill component processes - -`Halo.fork child` starts a process owned by the current React activation and returns a `ForkId`. The process may outlive the handler that created it: - -```purescript -startSearch query = do - previous <- gets _.searchFiber - traverse_ Halo.kill previous - - fiber <- Halo.fork do - modify_ _ { loading = true } - results <- lift (Search.run query) - modify_ _ { loading = false, results = results } - - modify_ _ { searchFiber = Just fiber } -``` - -`Halo.kill id` removes a tracked fork, fences it synchronously, requests Aff cancellation, and waits for cancellation and Aff finalizers before returning. Killing an unknown or completed ID does nothing. - -A killed or deactivated root cannot commit Halo state, register another Halo-owned capability, or start a newly lifted application effect—even if it catches the initial Aff cancellation. Cancellation cannot retract an HTTP request, storage write, callback, or log that already happened. Design external writes for retry and idempotency where needed. - -## Clean up at the correct boundary - -Halo intentionally has no asynchronous `onDeactivate` handler. React effect cleanup is synchronous, so React cannot wait for a `HaloM`, `AppM`, or `Aff` callback. Starting detached work during cleanup would also escape component ownership. - -Choose cleanup according to the resource: - -- **Component process:** acquire and use the resource inside `fork` with an Aff finalizer. Deactivation requests cancellation of the fork. -- **Event source:** return synchronous cleanup from `makeEmitter`; Halo runs it while deactivating the subscription scope. -- **Other synchronous resource:** call `registerCleanup cleanup`. Call `releaseCleanup id` to remove and run it early. -- **User cancellation:** retain the `ForkId` and call `kill`, which waits for finalizers. -- **Persistence:** save during normal application flow. Do not rely on unmount completing asynchronous persistence. - -`registerCleanup` accepts only `Effect Unit`, not `HaloM`, AppM, or `Aff`. `releaseCleanup` removes tracking before invoking the effect, so a throw is reported in the current root's error context and is not retried. Unknown and already released IDs are ignored. - -Deactivation first fences the activation, then attempts every generic cleanup and subscription cleanup before requesting cancellation of handlers, forks, and tasks. A cleanup throw is reported as `DeactivationError` through the latest `onError` without blocking the other resources. No ordering between generic and subscription cleanup is part of the API. React cannot wait for Aff cancellation, but finalizers cannot commit Halo state or begin new lifted effects after the fence. - -## Run independent work in parallel - -`HaloM` has a direct `Parallel` instance with abstract counterpart `HaloAp`. Parallel branches share one root, component scope, and interpreter snapshot: - -```purescript -loadDashboard = do - Tuple profile feed <- sequential ado - profile <- parallel (lift Profile.load) - feed <- parallel (lift Feed.load) - in Tuple profile feed - - modify_ _ { profile = profile, feed = feed } -``` +### [Managed work](guide/managed-work.md) -Prefer independent application reads followed by one Halo state update. Concurrent state writes have nondeterministic ordering and can overwrite one another. +Use this chapter to choose among handler-owned work, tasks, and forks: -Parallel work is lexical: the combined computation waits for every branch. Use `fork` when work must continue independently of the launching handler or needs explicit cancellation by ID. +| Requirement | Mechanism | +|---|---| +| Work belongs to one dispatched action | handler | +| Rendering needs typed lifecycle state | task | +| Work must outlive its launching handler | fork | +| Cancellation must await finalizers | `Task.reset` or `Halo.kill` | -## Configure lifecycle handlers +### [Lifecycle and resources](guide/lifecycle-and-resources.md) -```purescript -type Handlers props state action m = - { onActivate :: HaloM props state action m Unit - , onPropsChange :: props -> HaloM props state action m Unit - , onAction :: action -> HaloM props state action m Unit - } -``` - -Start with `defaultHandlers` and replace only the callbacks the component needs. - -### `onActivate` - -Halo calls this for every React effect activation. Development StrictMode can run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable, not as an exactly-once mount event. - -### `onPropsChange previousProps` - -Halo starts this when the props reference changes. Read current props with `getProps`. The root uses the latest handlers and interpreter supplied to the hook. +Use this chapter for activation, StrictMode replay, subscriptions, cleanup, and unexpected errors. -### `onAction action` - -Halo starts one root for each action dispatched while active, including actions emitted by subscriptions. Dispatch while inactive is ignored. - -## Subscribe to event sources - -Halo's emitter API avoids a Halogen dependency: - -```purescript -events = Halo.makeEmitter \emit -> do - listener <- source.listen emit - pure (source.remove listener) +```text +component or useHalo + └─ action handler + ├─ ordinary HaloM work + ├─ managed task + └─ component-owned fork ``` -`subscribe events` registers an action source in the current activation. `subscribeWithId (\id -> emitterFor id)` also exposes the allocated `SubscriptionId`. `unsubscribe id` removes tracking before running cleanup. - -Deactivation attempts every tracked cleanup even when one throws. Cleanup failures are reported as `DeactivationError` without preventing the remaining cleanup and cancellation requests. A callback retained by a faulty source stays bound to its original activation and cannot dispatch into a later StrictMode activation. - -Emitters broadcast actions. They do not provide backpressure, consuming queues, or scheduling policies. - -## Handle unexpected errors - -Every component or hook spec supplies: - -```purescript -onError :: ErrorContext props action -> Error -> Effect Unit -``` - -Contexts identify the failed root or cleanup: - -- `ActivationError` for `onActivate`; -- `PropsChangeError previousProps` for `onPropsChange`; -- `ActionError action` for `onAction`; -- `ForkError id` for a component-owned fork; and -- `DeactivationError` for throwing subscription cleanup. - -Halo selects the latest `onError` callback when reporting a failure. Expected domain failures belong in application values, actions, or state. Halo-initiated cancellation is suppressed after its root is fenced. - -## Choose `component` or `useHalo` - -Use `Halo.component` when Halo owns the complete component. Its renderer receives `{ props, state, tasks, dispatch }`. `initialState` receives initial props once per mount; synchronize later prop changes in `onPropsChange`. - -Use `Halo.useHalo` when other React hooks share the render function. It accepts the same application interpreter and returns `{ state, tasks, dispatch }`. - -## Common mistakes +Every handler, task, fork, subscription, and registered cleanup belongs to one React activation. Deactivation fences that activation before cleanup and cancellation requests begin. -- **An activation runs twice in development:** React StrictMode replayed setup. Make `onActivate` repeatable. -- **A long-running action cannot be cancelled by another action:** move that work into `fork` and retain its `ForkId`. -- **A state update overwrites newer input:** capture only effect inputs before waiting, then update current state with `modify_`. -- **Cleanup needs asynchronous work:** use an Aff finalizer in a component-owned fork; React cannot await an asynchronous deactivation callback. -- **An old event callback still fires:** Halo rejects its dispatch if the activation is stale, but the external source must still implement cleanup correctly. +Maintainers changing these guarantees should also read the [runtime architecture](architecture.md). diff --git a/docs/guide/actions-and-state.md b/docs/guide/actions-and-state.md new file mode 100644 index 0000000..edbfd46 --- /dev/null +++ b/docs/guide/actions-and-state.md @@ -0,0 +1,111 @@ +# Actions, effects, and state + +A dispatched action starts an independent handler in the current React activation. Handlers can read current state and props, update state, and lift application logic. + +## Dispatch a typed action + +Keep UI events in an action ADT rather than calling asynchronous logic from rendering: + +```purescript +data Action + = NameChanged String + | Save + +render { dispatch } = + R.input + { onChange: handler targetValue (dispatch <<< NameChanged) + , value: "" + } +``` + +Each call to `dispatch` starts `onAction` synchronously. Dispatch while the component is inactive is ignored. + +## Handle actions independently + +Use `defaultHandlers` when only some lifecycle callbacks matter: + +```purescript +handlers = Halo.defaultHandlers + { onAction = case _ of + NameChanged name -> + modify_ _ { name = name } + + Save -> + saveCurrentForm + } +``` + +Handlers overlap. A long-running `Save` does not block a later `NameChanged`. If later actions must cancel or supersede work, use a [managed task or fork](managed-work.md) rather than relying on handler ordering. + +## Lift application effects + +Application logic stays in AppM and enters Halo through standard transformer `lift`: + +```purescript +saveCurrentForm = do + form <- gets _.form + result <- lift (Form.save form) + modify_ _ { saveResult = Just result } +``` + +Every handler captures the interpreter current when it starts. A later React render may supply a newer interpreter for new handlers, but it does not change one already running. + +## Update current state + +`HaloM` has `MonadState state`, so normal state operations are available: + +```purescript +name <- gets _.name +modify_ _ { submittedName = Just name } +``` + +State operations use the state current at that operation. Avoid restoring a whole snapshot after asynchronous work: + +```purescript +-- Avoid: this can overwrite a newer action's changes. +old <- get +result <- lift (save old.form) +put (old { result = Just result }) +``` + +Capture only the effect input, then update current state: + +```purescript +form <- gets _.form +result <- lift (save form) +modify_ _ { result = Just result } +``` + +This is especially important because action handlers, tasks, and forks may overlap. + +## Read current and previous props + +`onPropsChange` receives the previous props. `getProps` reads the current props: + +```purescript +handlers = Halo.defaultHandlers + { onPropsChange = \previous -> do + current <- Halo.getProps + synchronize previous current + } +``` + +Capture props before waiting when the work must use one render's value. Otherwise, a later `getProps` intentionally observes newer props. + +## Run independent effects in parallel + +`HaloM` has a direct `Parallel` instance. Run independent application reads in parallel, then update component state once: + +```purescript +loadDashboard = do + Tuple profile feed <- sequential ado + profile <- parallel (lift Profile.load) + feed <- parallel (lift Feed.load) + in Tuple profile feed + + modify_ _ { profile = profile, feed = feed } +``` + +Parallel branches share one root, activation, error context, and interpreter snapshot. Concurrent component-state writes have nondeterministic ordering, so prefer combining results before one update. + +Parallel work is lexical: the surrounding computation waits for every branch. Use a [fork](managed-work.md#use-a-fork-for-an-independent-process) when work must continue beyond its launching handler. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..ab2b4f4 --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,143 @@ +# Getting started + +Halo has two React entry points. Choose one first; their action, state, and application-effect model is otherwise the same. + +## Use `component` for a complete component + +`component` creates a React component from one Halo specification: + +```purescript +loadButton :: Env -> Component Props +loadButton env = + Halo.component "LoadButton" (runAppM env) + { initialState: \_ -> initialState + , handlers + , onError + , render + } +``` + +The specification supplies: + +- initial state derived from the initial props; +- action handlers; +- unexpected-error reporting; and +- a renderer receiving `{ props, state, tasks, dispatch }`. + +Later prop changes do not recreate state. They start `onPropsChange` instead. + +## Use `useHalo` with other hooks + +`useHalo` fits the same runtime into a component that owns its render function: + +```purescript +halo <- Halo.useHalo (runAppM env) + { props + , initialState + , handlers + , onError + } + +pure $ render + { props + , state: halo.state + , tasks: halo.tasks + , dispatch: halo.dispatch + } +``` + +The hook returns current state, an immutable managed-task view, and synchronous dispatch. Use ordinary React hooks alongside it as needed. + +## Define state and actions + +State contains the values used for rendering. An action ADT describes events that rendering can send to Halo: + +```purescript +type Props = { title :: String } + +type State = + { greeting :: Maybe String + } + +initialState :: State +initialState = + { greeting: Nothing + } + +data Action = LoadGreeting +``` + +## Render state and dispatch actions + +Rendering is ordinary `react-basic` code. Calling `dispatch` starts `handlers.onAction` in the active component scope: + +```purescript +render { props, state, dispatch } = + R.div_ + [ R.text props.title + , R.button + { onClick: capture_ (dispatch LoadGreeting) + , children: [ R.text "Load" ] + } + , R.text $ fromMaybe "Not loaded" state.greeting + ] +``` + +Dispatch is synchronous; the handler it starts may continue asynchronously. + +## Handle the action + +Start with `defaultHandlers` and replace only the callbacks the component needs: + +```purescript +handlers :: Halo.Handlers Props State Action AppM +handlers = Halo.defaultHandlers + { onAction = case _ of + LoadGreeting -> do + greeting <- lift loadGreeting + modify_ _ { greeting = Just greeting } + } +``` + +`HaloM` supplies component state and props. Standard transformer `lift` runs application logic through the interpreter passed to `component` or `useHalo`. + +## Define the application boundary + +Application services remain in the application's own monad: + +```purescript +type Env = + { loadGreeting :: Aff String + } + +newtype AppM a = AppM (ReaderT Env Aff a) + +runAppM :: Env -> AppM ~> Aff +runAppM env (AppM program) = runReaderT program env + +loadGreeting :: AppM String +loadGreeting = AppM do + env <- ask + liftAff env.loadGreeting +``` + +The component computation type makes both layers explicit: + +```purescript +type UI a = Halo.HaloM Props State Action AppM a +``` + +The interpreter must return the `Aff` that performs the work. Do not detach it with `launchAff_`; Halo can own and cancel only the returned computation. + +## Report unexpected errors + +Both entry points require a synchronous error callback: + +```purescript +onError _ error = + Console.error $ message error +``` + +Expected domain failures should remain typed application values or component state. `onError` is for unexpected exceptions from a current Halo-owned root or synchronous cleanup. + +Next, see [Actions, effects, and state](actions-and-state.md) for overlapping handlers, safe state updates, props changes, and parallel work. diff --git a/docs/guide/lifecycle-and-resources.md b/docs/guide/lifecycle-and-resources.md new file mode 100644 index 0000000..76a287c --- /dev/null +++ b/docs/guide/lifecycle-and-resources.md @@ -0,0 +1,136 @@ +# Lifecycle and resources + +One Halo runtime may pass through multiple React effect activations during development StrictMode. Handlers, forks, tasks, subscriptions, and registered cleanup belong to the activation that created them. + +## Configure lifecycle handlers + +Start with `defaultHandlers` and replace the callbacks the component uses: + +```purescript +handlers = Halo.defaultHandlers + { onActivate = initialize + , onPropsChange = synchronizeProps + , onAction = handleAction + } +``` + +`onActivate` runs for every actual activation: + +```purescript +onActivate = + Task.once initializationSlot initialize +``` + +Development StrictMode may run setup, cleanup, then setup again for one hook instance. Treat activation as repeatable, not as an exactly-once mount event. + +`onPropsChange` receives the previous props; read current props with `getProps`: + +```purescript +onPropsChange previous = do + current <- Halo.getProps + synchronize previous current +``` + +`onAction` runs once for each dispatch in the active scope: + +```purescript +onAction = case _ of + Submitted -> submit + Cancelled -> cancel +``` + +Each invocation is an independent root using the latest handlers and application interpreter supplied to the React boundary. + +## Turn external events into actions + +Create an emitter from registration logic. Registration returns synchronous cleanup for that receiver: + +```purescript +names :: Halo.Emitter String +names = Halo.makeEmitter \emit -> + source.listen emit +``` + +`Emitter` has a `Functor` instance, so map source values into the component action type before subscribing: + +```purescript +actions :: Halo.Emitter Action +actions = NameChanged <$> names + +onActivate = + void $ Halo.subscribe actions +``` + +Mapping changes emitted values without changing source registration or cleanup. Emitters deliberately have no `Applicative` instance because combining event sources would require a synchronization policy such as zip or latest-value. + +Use `subscribeWithId` when normal component flow must unsubscribe early: + +```purescript +subscriptionId <- Halo.subscribeWithId \_ -> actions +Halo.unsubscribe subscriptionId +``` + +Manual unsubscribe removes tracking before running cleanup. Deactivation attempts cleanup for every subscription that remains registered. A retained callback stays bound to its original activation and cannot dispatch into a later StrictMode activation. + +Emitters broadcast actions. They do not provide backpressure, consuming queues, or scheduling policies. + +## Register synchronous cleanup + +Use `registerCleanup` for a synchronous resource that is not an emitter subscription: + +```purescript +cleanupId <- Halo.registerCleanup removeListener +``` + +Release it early when normal component flow no longer needs the resource: + +```purescript +Halo.releaseCleanup cleanupId +``` + +Release removes the entry before invoking it. Unknown and already released IDs are ignored, and a throwing release is not retried during deactivation. + +## Put asynchronous cleanup in an Aff finalizer + +React effect cleanup is synchronous, so Halo intentionally has no asynchronous `onDeactivate` handler. Acquire and release an asynchronous resource inside owned Aff work: + +```purescript +void $ Halo.fork do + liftAff $ Aff.bracket acquire release use +``` + +Deactivation requests cancellation of the fork, which runs its Aff finalizer. React does not wait for that cancellation, so persistence or other required writes should happen during normal application flow rather than depending on unmount. + +## Understand deactivation order + +Deactivation first fences the activation. It then attempts synchronous registered and subscription cleanup before requesting cancellation of handlers, tasks, and forks. + +```purescript +onError DeactivationError error = + reportCleanupFailure error +``` + +One throwing cleanup does not prevent the remaining cleanup or cancellation requests. No ordering between generic and subscription cleanup is part of the API. + +A stale root cannot commit component state, register another Halo capability, or begin a newly lifted application effect after the fence—even if it catches the initial Aff cancellation. + +## Report unexpected errors + +Both React entry points require: + +```purescript +onError :: ErrorContext props action -> Error -> Effect Unit +``` + +Pattern match on the context when reporting needs different metadata: + +```purescript +onError context error = case context of + ActivationError -> report "activation" error + PropsChangeError previous -> reportProps previous error + ActionError action -> reportAction action error + ForkError forkId -> reportFork forkId error + DeactivationError -> report "cleanup" error +``` + +Halo reads the latest callback when an unexpected current failure is reported. Expected domain failures belong in application values, actions, or task outcomes. Halo-initiated cancellation is suppressed after its root is fenced. diff --git a/docs/guide/managed-work.md b/docs/guide/managed-work.md new file mode 100644 index 0000000..deaed7d --- /dev/null +++ b/docs/guide/managed-work.md @@ -0,0 +1,158 @@ +# Managed work + +Leave work in its action handler when no other action needs to identify it and rendering does not need a typed lifecycle. Add a task for renderable outcomes or a fork for an independent component process. + +## Start with handler-owned work + +A handler is already owned and cancelled by its React activation: + +```purescript +Refresh -> do + account <- lift Account.load + modify_ _ { account = Just account } +``` + +No fork is needed merely because the application effect is asynchronous. + +## Use a task for typed lifecycle state + +A task field retains `Idle`, `Active`, a typed failure, or a typed result in component state: + +```purescript +type State = + { search :: Task.State SearchError Results + , query :: String + } + +searchSlot :: Task.Slot "search" State SearchError Results +searchSlot = Task.slot (Proxy :: Proxy "search") + +initialState = + { search: Task.idle searchSlot + , query: "" + } +``` + +`Task.slot` uses its type-level label as both record field and identity. Use `slotAt` only for a nested or custom lawful focus: + +```purescript +nestedSearchSlot = + Task.slotAt (Proxy :: Proxy "nestedSearch") + (tasksLens <<< searchLens) +``` + +One slot name must identify one focus for the runtime lifetime. Reusing a name for another lens—or giving one focus two names—is invalid and fails on first use before state mutation or cancellation: + +```purescript +-- Invalid if `searchSlot` already names another focus. +duplicate = Task.slotAt (Proxy :: Proxy "search") otherLens +``` + +A task body is ordinary `HaloM` returning `Either error result`: + +```purescript +Search query -> Task.supersede searchSlot do + modify_ _ { query = query } + lift (Search.run query) +``` + +A renderer receives `tasks :: Task.View State` beside component state: + +```purescript +case Task.toStatus tasks searchSlot of + Task.Idle -> renderPrompt + Task.Active -> renderSpinner + Task.Failed error -> renderError error + Task.Succeeded results -> renderResults results +``` + +`Task.toMaybe tasks slot` returns only a successful result. `Task.isActive tasks slot` covers both a debounce timer and an executing body. The status prisms can inspect a projected status: + +```purescript +result = preview Task._Succeeded (Task.toStatus tasks searchSlot) +``` + +State and task view come from one coherent render snapshot. `Task.State` is freely copyable, but a copied, stale, cross-slot, or cross-runtime active value has no matching authority and projects `Idle`; it cannot cross-cancel or commit over current work. + +## Choose a task policy + +Use `once` for initialization that remains terminal until reset: + +```purescript +Task.once initializationSlot initialize +``` + +Use `startIfInactive` when another invocation should run after either success or typed failure, but not while work is active: + +```purescript +Task.startIfInactive saveSlot save +``` + +Use `supersede` when every new invocation should become authoritative immediately: + +```purescript +Task.supersede searchSlot (search query) +``` + +Use `debounce` for trailing-edge latest-wins interaction: + +```purescript +Task.debounce searchSlot (Milliseconds 250.0) (search query) +``` + +Its private timer and body both render as `Active`. A new invocation cancels either phase. + +Use `reset` to clear terminal state or cancel active work: + +```purescript +Task.reset searchSlot +``` + +Reset publishes `Idle`, fences active work, and waits for its Aff finalizers. Supersession fences old work immediately but does not wait for its finalizers before starting the replacement. + +Expected failures belong in `Either`. An unexpected exception returns the matching current task to `Idle` and follows Halo's `ForkError` reporting path. Cancellation is neither a typed failure nor an unexpected error. + +Put retry policy in AppM and lift the retrying computation. Halo does not add retry, timestamps, or a generic scheduler to task state. Task state is component-owned result storage, not a global cache, and no computation or input is retained for reruns. + +## Use a fork for an independent process + +A fork has its own component-owned identity and may outlive the handler that starts it. Tasks and forks inherit the application interpreter captured by the handler that launches them: + + +```purescript +startSynchronization = do + fiber <- Halo.fork synchronize + modify_ _ { synchronization: Just fiber } +``` + +Retain its `ForkId` when another action must cancel it: + +```purescript +cancelSynchronization = do + current <- gets _.synchronization + traverse_ Halo.kill current + modify_ _ { synchronization = Nothing } +``` + +`kill` removes and fences the fork synchronously, then waits for Aff cancellation and finalizers. Killing an unknown or completed ID does nothing. + +Use an Aff finalizer for asynchronous resource release inside owned work: + +```purescript +connectionProcess = Halo.fork do + liftAff $ Aff.finally useConnection closeConnection +``` + +React cannot await deactivation cleanup, but the ownership fence prevents a cancelled finalizer from committing Halo state or beginning another lifted application effect. + +## Choose by observable behavior + +| Requirement | Use | +|---|---| +| One action owns the result | handler | +| Rendering needs typed lifecycle state | task | +| A newer call replaces an older call | `supersede` or `debounce` | +| Work continues after its handler returns | fork | +| Cancellation must await finalizers | `reset` or `kill` | + +Cancellation cannot retract an HTTP request, storage write, callback, or log that already happened. External writes still need appropriate idempotency or retry semantics. diff --git a/test/Test/Halo/DocExamples.purs b/test/Test/Halo/DocExamples.purs index e75de37..ebb4a5b 100644 --- a/test/Test/Halo/DocExamples.purs +++ b/test/Test/Halo/DocExamples.purs @@ -3,6 +3,7 @@ module Test.Halo.DocExamples where import Prelude import Control.Monad.Reader (ReaderT, ask, runReaderT) +import Control.Monad.State (modify_) import Control.Monad.Trans.Class (lift) import Data.Either (Either(..)) import Effect.Aff (Aff) @@ -39,50 +40,37 @@ loadGreeting = AppM do type Props = { title :: String } -type State = - { greeting :: Task.State String String - } +type State = { greeting :: String } -greetingSlot :: Task.Slot "greeting" State String String -greetingSlot = Task.slot (Proxy :: Proxy "greeting") +initialState :: State +initialState = { greeting: "Not loaded" } -data Action - = Load - | Cancel +data Action = LoadGreeting type UI a = Halo.HaloM Props State Action AppM a handlers :: Halo.Handlers Props State Action AppM handlers = Halo.defaultHandlers { onAction = case _ of - Load -> Task.supersede greetingSlot do + LoadGreeting -> do greeting <- lift loadGreeting - pure (Right greeting) - Cancel -> Task.reset greetingSlot + modify_ _ { greeting = greeting } } loadButton :: Env -> Component Props loadButton env = Halo.component "LoadButton" (runAppM env) - { initialState: \_ -> { greeting: Task.idle greetingSlot } + { initialState: \_ -> initialState , handlers , onError: \_ error -> - Console.error $ "Unexpected Halo error: " <> message error - , render: \{ props, tasks, dispatch } -> + Console.error $ message error + , render: \{ props, state, dispatch } -> R.div_ [ R.text props.title , R.button - { onClick: capture_ (dispatch Load) - , children: [ R.text if Task.isActive tasks greetingSlot then "Restart" else "Load" ] - } - , R.button - { onClick: capture_ (dispatch Cancel) - , children: [ R.text "Cancel" ] + { onClick: capture_ (dispatch LoadGreeting) + , children: [ R.text "Load" ] } - , R.text $ case Task.toStatus tasks greetingSlot of - Task.Idle -> "Not loaded" - Task.Active -> "Loading…" - Task.Failed error -> error - Task.Succeeded greeting -> greeting + , R.text state.greeting ] } @@ -92,7 +80,28 @@ useExample -> Hook (Halo.UseHalo Props State Action AppM) (Halo.HaloResult State Action) useExample env props = Halo.useHalo (runAppM env) { props - , initialState: { greeting: Task.idle greetingSlot } + , initialState , handlers , onError: \_ _ -> pure unit } + +type SearchState = { search :: Task.State String String } + +searchSlot :: Task.Slot "search" SearchState String String +searchSlot = Task.slot (Proxy :: Proxy "search") + +data SearchAction = Search String + +searchHandler + :: SearchAction + -> Halo.HaloM Unit SearchState SearchAction Aff Unit +searchHandler = case _ of + Search query -> Task.supersede searchSlot do + pure (Right query) + +renderSearch :: Task.View SearchState -> String +renderSearch tasks = case Task.toStatus tasks searchSlot of + Task.Idle -> "Search" + Task.Active -> "Searching" + Task.Failed error -> error + Task.Succeeded result -> result diff --git a/test/Test/Halo/GuideExamples.purs b/test/Test/Halo/GuideExamples.purs index 10228ff..3018c20 100644 --- a/test/Test/Halo/GuideExamples.purs +++ b/test/Test/Halo/GuideExamples.purs @@ -2,12 +2,18 @@ module Test.Halo.GuideExamples where import Prelude -import Control.Monad.State (modify_) +import Control.Monad.State (gets, modify_) import Control.Monad.Trans.Class (lift) import Control.Parallel (parallel, sequential) import Data.Either (Either(..)) +import Data.Foldable (traverse_) +import Data.Lens (Lens', preview) +import Data.Lens.Record (prop) +import Data.Maybe (Maybe(..)) import Data.Tuple (Tuple(..)) +import Effect (Effect) import Effect.Aff (Aff, Milliseconds(..)) +import Effect.Exception as Exception import React.Halo as Halo import React.Halo.Task as Task import Type.Proxy (Proxy(..)) @@ -26,40 +32,56 @@ parallelExample = do modify_ _ { profile = profile, feed = feed } -data SimpleAction = InitializeData +data FormAction + = NameChanged String + | Save -simpleEmitter :: Halo.Emitter SimpleAction -simpleEmitter = Halo.makeEmitter \_ -> pure (pure unit) +type FormState = + { name :: String + , savedName :: Maybe String + } -simpleSubscription :: Halo.HaloM Unit Unit SimpleAction Aff Unit -simpleSubscription = void $ Halo.subscribeWithId \_ -> simpleEmitter +formHandler :: FormAction -> Halo.HaloM Unit FormState FormAction Aff Unit +formHandler = case _ of + NameChanged name -> modify_ _ { name = name } + Save -> do + name <- gets _.name + saved <- lift (pure name :: Aff String) + modify_ _ { savedName = Just saved } -genericCleanup :: Halo.HaloM Unit Unit SimpleAction Aff Unit -genericCleanup = do - cleanupId <- Halo.registerCleanup (pure unit) - Halo.releaseCleanup cleanupId +propsHandler + :: { value :: Int } + -> Halo.HaloM { value :: Int } Unit Unit Aff Unit +propsHandler previous = do + current <- Halo.getProps + void $ lift (pure $ previous.value + current.value :: Aff Int) type SearchState = { query :: String , search :: Task.State String Int } -data SearchAction = Search String | CancelSearch - searchSlot :: Task.Slot "search" SearchState String Int searchSlot = Task.slot (Proxy :: Proxy "search") -retryingSearch :: String -> Aff (Either String Int) -retryingSearch _ = pure (Right 1) +initialSearchState :: SearchState +initialSearchState = + { query: "" + , search: Task.idle searchSlot + } + +search :: String -> Halo.HaloM Unit SearchState Unit Aff (Either String Int) +search query = do + modify_ _ { query = query } + lift (pure (Right 1) :: Aff (Either String Int)) -searchHandler - :: SearchAction - -> Halo.HaloM Unit SearchState SearchAction Aff Unit -searchHandler = case _ of - Search query -> Task.debounce searchSlot (Milliseconds 250.0) do - modify_ _ { query = query } - lift (retryingSearch query) - CancelSearch -> Task.reset searchSlot +taskPolicies :: Halo.HaloM Unit SearchState Unit Aff Unit +taskPolicies = do + Task.once searchSlot (search "initial") + Task.startIfInactive searchSlot (search "save") + Task.supersede searchSlot (search "latest") + Task.debounce searchSlot (Milliseconds 250.0) (search "debounced") + Task.reset searchSlot renderSearch :: Task.View SearchState -> String renderSearch tasks = case Task.toStatus tasks searchSlot of @@ -67,3 +89,72 @@ renderSearch tasks = case Task.toStatus tasks searchSlot of Task.Active -> "Searching" Task.Failed error -> error Task.Succeeded _ -> "Done" + +successfulSearch :: Task.View SearchState -> Maybe Int +successfulSearch tasks = preview Task._Succeeded (Task.toStatus tasks searchSlot) + +type NestedState = + { tasks :: { search :: Task.State String Int } + } + +tasksLens :: Lens' NestedState { search :: Task.State String Int } +tasksLens = prop (Proxy :: Proxy "tasks") + +nestedSearchLens :: Lens' { search :: Task.State String Int } (Task.State String Int) +nestedSearchLens = prop (Proxy :: Proxy "search") + +nestedSearchSlot :: Task.Slot "nestedSearch" NestedState String Int +nestedSearchSlot = Task.slotAt (Proxy :: Proxy "nestedSearch") (tasksLens <<< nestedSearchLens) + +type ProcessState = { synchronization :: Maybe Halo.ForkId } + +startSynchronization :: Halo.HaloM Unit ProcessState Unit Aff Unit +startSynchronization = do + fiber <- Halo.fork (lift (pure unit :: Aff Unit)) + modify_ _ { synchronization = Just fiber } + +cancelSynchronization :: Halo.HaloM Unit ProcessState Unit Aff Unit +cancelSynchronization = do + current <- gets _.synchronization + traverse_ Halo.kill current + modify_ _ { synchronization = Nothing } + +data ExternalAction = NameReceived String + +names :: Halo.Emitter String +names = Halo.makeEmitter \emit -> do + emit "Halo" + pure (pure unit) + +actions :: Halo.Emitter ExternalAction +actions = NameReceived <$> names + +subscriptionExample :: Halo.HaloM Unit Unit ExternalAction Aff Unit +subscriptionExample = do + subscriptionId <- Halo.subscribeWithId \_ -> actions + Halo.unsubscribe subscriptionId + +cleanupExample + :: Effect Unit + -> Halo.HaloM Unit Unit ExternalAction Aff Unit +cleanupExample removeListener = do + cleanupId <- Halo.registerCleanup removeListener + Halo.releaseCleanup cleanupId + +lifecycleHandlers :: Halo.Handlers Unit Unit ExternalAction Aff +lifecycleHandlers = Halo.defaultHandlers + { onActivate = void $ Halo.subscribe actions + , onPropsChange = \_ -> void Halo.getProps + , onAction = \_ -> pure unit + } + +errorExample + :: Halo.ErrorContext Unit ExternalAction + -> Exception.Error + -> Effect Unit +errorExample context _ = case context of + Halo.ActivationError -> pure unit + Halo.PropsChangeError _ -> pure unit + Halo.ActionError _ -> pure unit + Halo.ForkError _ -> pure unit + Halo.DeactivationError -> pure unit