diff --git a/.npmignore b/.npmignore deleted file mode 100644 index fbd7333..0000000 --- a/.npmignore +++ /dev/null @@ -1,52 +0,0 @@ -# Dependencies -node_modules/ -bun.lock - -# Build artifacts -*.tsbuildinfo - -# Build tools configuration -tsconfig.json -tsconfig.build.json -biome.json -eslint.config.js -context7.json -.prettierrc -.editorconfig - -# Tests, benchmarks, and internal documentation -test/ -bench/ -docs/ -adr/ -GUIDE.md -REQUIREMENTS.md -RECIPES.md -REACT_INTEGRATION.md -CHANGELOG.md - -# IDE and editor files -.agents/ -.claude/ -.cursorrules -.DS_Store -.vibe/ -.zed/ - -# GitHub -.github/ -.gitignore - -# OS and temporary files -*.log -*.tmp - -# Keep these essential files for published package -!index.js -!index.ts -!index.dev.js -!src/ -!types/ -!package.json -!README.md -!LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md index 614ada7..9d2b1b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,23 @@ # Changelog -## [Unreleased] +## 1.4.0 + +### Changed + +- **Package distribution metadata overhauled** (`package.json`): The published entry point is now the *unminified* ESM bundle (`index.js`) instead of a minified artifact — consumers' bundlers minify anyway, and readable source improves debugging and bug reports. An explicit `exports` map (`.`: `types` → `bun` → `default`) replaces the bare `main`/`module` pair: the `"bun"` condition resolves TypeScript source directly for Bun consumers, while `"default"` serves the bundled `index.js` to all other toolchains. The `"module": "index.ts"` field is removed — it pointed at raw TypeScript, which broke webpack and older Rollup configs. TypeScript is now an *optional* peer dependency (`peerDependenciesMeta`), so JS-only consumers no longer get an unresolvable-peer warning. A `files` allowlist replaces the fragile `.npmignore` negation patterns; the unused `index.dev.js` artifact is removed. **Migration:** This is a **minor** version bump. The `exports` map blocks deep imports into package internals — code relying on these must switch to named imports from the package root. If your toolchain read the `module` field to consume TypeScript source, switch to the `"bun"` exports condition or import the bundled `index.js`. ### Added -- **`EffectConvergenceError`**: New error class (exported from the package root), thrown when queued effects keep re-triggering each other without settling within 1000 flush passes. Typical triggers: an effect that unconditionally writes a signal it reads (`createEffect(() => count.set(count.get() + 1))`), or two effects that write each other's dependencies. The error surfaces synchronously from the `set()`/`update()`/`batch()`/`createEffect()` call that triggered the runaway; other queued effects still run before it is thrown. +- **`EffectConvergenceError`**: New error class (exported from the package root), thrown when queued effects keep re-triggering each other without settling within 1000 flush passes. Typical triggers: an effect that unconditionally writes a signal it reads (`createEffect(() => count.set(count.get() + 1))`), or two effects that write each other's dependencies. The error surfaces synchronously from the call that triggered the runaway; other queued effects still run before it is thrown. - **`InvalidStoreMutationError`**: New error class (exported from the package root), a `TypeError` subclass thrown when a `Store` property is assigned, deleted, or defined directly via the proxy (`store.name = 'Bob'`, `delete store.name`, `Object.defineProperty(store, …)`, `Object.assign(store, …)`). The message names the correct reactive alternative (`store.key.set(value)`, `store.set(next)`, `store.add(key, value)`, or `store.remove(key)`). See [ADR-0017](adr/0017-store-proxy-rejects-direct-writes.md). ### Fixed -- **Direct property assignment on a `Store` proxy silently corrupted the store** (`src/nodes/store.ts`): Previously, the proxy defined no `set`, `deleteProperty`, or `defineProperty` traps — so `store.name = 'Bob'` wrote the raw value onto the proxy target object, shadowing the child `State` signal. From then on, `store.name` returned the raw value while `store.get()` returned the reactive value, and the two diverged silently. `delete store.name` similarly bypassed `remove()` semantics. Now the three traps throw `InvalidStoreMutationError`, leaving `store.get()` and the child signal intact. **Migration:** code that previously assigned through the proxy (and silently corrupted state) must use the reactive API (`store.key.set()`, `store.set()`, `store.add()`, `store.remove()`). This is a **minor** version bump, not a patch — the throw replaces previously-undetected silent corruption. See [ADR-0017](adr/0017-store-proxy-rejects-direct-writes.md). -- **A throwing effect no longer skips sibling effects in the same flush** (`src/graph.ts`): Previously, an exception from one effect's callback propagated out of `flush()` immediately — all effects queued after it were silently skipped (their DOM updates and subscriptions lost until some arbitrary later flush), and the throwing effect was left stuck with `FLAG_RUNNING` set, so its next `refresh()` threw a spurious `CircularDependencyError`. Now `flush()` catches per-effect errors, drains the entire queue, and rethrows after: a single error is rethrown as-is (error identity preserved for existing `catch` code), multiple errors are wrapped in an `AggregateError`. `runEffect()` clears `FLAG_RUNNING` in its `finally`, so a previously-throwing effect re-runs normally on the next update. -- **Effects that write signals they depend on now converge** (`src/graph.ts`, `src/nodes/effect.ts`): Previously, `runEffect()` unconditionally overwrote the node's flags with `FLAG_CLEAN` after running, clobbering the dirty re-mark set by the effect's own write — so even a converging clamp effect (`if (v > 10) s.set(10)`) ended one run stale, having rendered the pre-clamp value while the signal held the clamped one; two subscribers of the same signal could disagree. Now `flush()` drains `queuedEffects` in passes over snapshots (effects re-queued during a pass run in the next pass), `propagate()` preserves `FLAG_RUNNING` on effects, and `runEffect()` preserves `FLAG_DIRTY`/`FLAG_CHECK` from the effect's own writes — a self-writing effect re-runs until the graph settles and its last run always observes the final signal values. Creation-time self-writes converge through the same path via a new internal `scheduleEffect()`, which also removes a re-entrant `runEffect` hazard (a write during an effect's creation run previously re-entered the still-running effect via the nested flush). -- **Mutual effect writes no longer hang the process** (`src/graph.ts`): Previously, two effects writing each other's dependencies re-queued each other forever inside one `flush()`, growing the queue until heap exhaustion — there was no loop guard. Now the flush-pass cap (1000) converts this into a loud `EffectConvergenceError` while sibling effects still run. -- **`List` and `Store` mutation methods leaked dependency edges into the caller's effect** (`src/nodes/list.ts`, `src/nodes/store.ts`, `src/nodes/collection.ts`): `List.set()`, `List.sort()`, `List.splice()`, `List.update()`, `Store.update()`, and `createCollection`'s `onChanges` change branch read child item signals without `untrack()`. Calling any of them inside an effect silently subscribed that effect to every item signal touched — causing over-broad re-runs on unrelated item mutations (persistent leak) or a spurious one-time re-run during setup (transient leak, removed by `trimSources` on the next run). `Store.set()` and `List.replace()` already had the correct `untrack` pattern; these methods now match it. The public read APIs (`get()`, `at()`, `byKey()`, `keys()`, `length`, iterator) remain deliberately tracking per [ADR-0015](adr/0015-composite-lookup-methods-track-structural-changes.md). +- **Direct property assignment on a `Store` proxy silently corrupted the store** (`src/nodes/store.ts`): Previously, the proxy defined no `set`, `deleteProperty`, or `defineProperty` traps — so `store.name = 'Bob'` wrote the raw value onto the proxy target, shadowing the child `State` signal. From then on `store.name` returned the raw value while `store.get()` returned the reactive value, diverging silently. Now the three traps throw `InvalidStoreMutationError`, leaving `store.get()` and the child signal intact. **Migration:** code that assigned through the proxy must use the reactive API (`store.key.set()`, `store.set()`, `store.add()`, `store.remove()`). This is a **minor** version bump — the throw replaces previously-undetected silent corruption. See [ADR-0017](adr/0017-store-proxy-rejects-direct-writes.md). +- **A throwing effect no longer skips sibling effects in the same flush** (`src/graph.ts`): Previously, an exception from one effect propagated out of `flush()` immediately — effects queued after it were silently skipped, and the throwing effect was left with `FLAG_RUNNING` set, causing its next `refresh()` to throw a spurious `CircularDependencyError`. Now `flush()` catches per-effect errors, drains the entire queue, and rethrows: a single error as-is (identity preserved), multiple errors wrapped in `AggregateError`. `runEffect()` clears `FLAG_RUNNING` in its `finally`, so a previously-throwing effect re-runs normally on the next update. +- **Effects that write signals they depend on now converge** (`src/graph.ts`, `src/nodes/effect.ts`): Previously, `runEffect()` overwrote the node's flags with `FLAG_CLEAN` after running, clobbering the dirty re-mark set by the effect's own write — so a converging clamp effect (`if (v > 10) s.set(10)`) ended one run stale, having rendered the pre-clamp value. Now `flush()` drains `queuedEffects` in passes over snapshots (effects re-queued during a pass run in the next pass), `propagate()` preserves `FLAG_RUNNING` on effects, and `runEffect()` preserves `FLAG_DIRTY`/`FLAG_CHECK` from the effect's own writes — a self-writing effect re-runs until the graph settles and its last run observes the final signal values. Creation-time self-writes converge via a new internal `scheduleEffect()`, which also removes a re-entrant `runEffect` hazard. +- **Mutual effect writes no longer hang the process** (`src/graph.ts`): Previously, two effects writing each other's dependencies re-queued each other forever inside one `flush()`, growing the queue until heap exhaustion. Now the flush-pass cap (1000) converts this into an `EffectConvergenceError` while sibling effects still run. +- **`List` and `Store` mutation methods leaked dependency edges into the caller's effect** (`src/nodes/list.ts`, `src/nodes/store.ts`, `src/nodes/collection.ts`): `List.set()`, `List.sort()`, `List.splice()`, `List.update()`, `Store.update()`, and `createCollection`'s `onChanges` change branch read child item signals without `untrack()`. Calling any of them inside an effect silently subscribed that effect to every item signal touched — causing over-broad re-runs on unrelated item mutations (persistent leak) or a spurious one-time re-run during setup (transient leak). These methods now wrap reads in `untrack()`, matching the pattern already used by `Store.set()` and `List.replace()`. The public read APIs (`get()`, `at()`, `byKey()`, `keys()`, `length`, iterator) remain deliberately tracking per [ADR-0015](adr/0015-composite-lookup-methods-track-structural-changes.md). ## 1.3.4 diff --git a/PLAN-ci-workflow.md b/PLAN-ci-workflow.md deleted file mode 100644 index 1086cf3..0000000 --- a/PLAN-ci-workflow.md +++ /dev/null @@ -1,124 +0,0 @@ -# PLAN: CI Workflow + Test Gate on Publish - -## Goal - -The repository has **no CI**. The only workflow (`.github/workflows/npm-publish.yml`) publishes to npm on GitHub release **without running a single test** — a release cut from a broken branch ships broken code with provenance attached. Add a CI workflow that runs typecheck, lint, tests, bundle-size regression, and build on every push/PR, and make the publish workflow run the same gate before publishing. - -All commands below were verified to pass locally on `next` (2026-07-10): - -- `bunx tsc --noEmit` → exit 0 -- `bunx biome lint .` → "Checked 49 files… No fixes applied." (exit 0) -- `bun test --path-ignore-patterns='**/regression*'` (what `bun run test` does) → 561 pass -- `bun test test/regression-bundle.test.ts` → 3 pass - -**Important pitfalls already checked:** `bun run lint` is `biome lint --write` (mutating — never use in CI), and `bunx biome ci .` FAILS on this repo (46 formatting errors — the repo is lint-clean but not biome-*format*-clean). CI must use `bunx biome lint .`, not `biome ci` and not `biome check`. - -## Files to touch - -- `.github/workflows/ci.yml` — new file -- `.github/workflows/npm-publish.yml` — add test gate before publish -- `package.json` — add a `check` convenience script (optional, step 3) - -## Implementation steps - -1. **Create `.github/workflows/ci.yml`:** - - ```yaml - name: CI - - on: - push: - branches: [main, next] - pull_request: - branches: [main, next] - - jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Typecheck - run: bunx tsc --noEmit - - - name: Lint - run: bunx biome lint . - - - name: Unit tests - run: bun run test - - - name: Bundle size regression - run: bun test test/regression-bundle.test.ts - - - name: Build - run: bun run build - - performance: - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - run: bun install --frozen-lockfile - - name: Performance regression (informational — shared runners are noisy) - run: bun test test/regression-performance.test.ts - ``` - - Match the existing workflow's indentation style (4 spaces, as in `npm-publish.yml`). - -2. **Gate the publish workflow.** In `.github/workflows/npm-publish.yml`, insert after the "Install all dependencies" step and **before** the "Build package" step: - - ```yaml - - name: Typecheck - run: bunx tsc --noEmit - - - name: Lint - run: bunx biome lint . - - - name: Run tests - run: bun run test - - - name: Bundle size regression - run: bun test test/regression-bundle.test.ts - ``` - - Keep everything else in that workflow unchanged (the provenance permissions block, version/tag detection, Node setup, `npm publish` step). - -3. **(Optional but recommended)** Add to `package.json` scripts so the gate is one command locally and in CI: - - ```json - "check": "bunx tsc --noEmit && bunx biome lint . && bun run test && bun test test/regression-bundle.test.ts" - ``` - - If added, CI steps 3–6 in `ci.yml` and the publish gate can each be collapsed to `bun run check` — but keep separate named steps in `ci.yml` for readable failure annotations; use the script only in `npm-publish.yml`. - -4. **Verify** by pushing to a branch and opening a draft PR against `next`; confirm both jobs run and the `test` job is green. Confirm the `performance` job is marked neutral/failed without failing the run (because of `continue-on-error`). - -## Edge cases a weaker model would likely miss - -- **`bun run test` already excludes regression tests** (`--path-ignore-patterns=**/regression*` in package.json) — do not run plain `bun test`, which would include the performance regression suite and make CI flaky. -- **Performance regression tests compare against the published stable release** (`@zeix/cause-effect-stable` npm alias) with a 20% margin and 1 ms floor (`test/regression-performance.test.ts`). On shared GitHub runners these timings are noisy — that's why the `performance` job is separate with `continue-on-error: true`. Do NOT put it in the required job, and do NOT delete it either; it's useful signal on large regressions. -- **Bundle regression is deterministic** (builds with `Bun.build` and measures bytes) — it belongs in the required job. -- **`biome ci` / `biome check` include formatting and fail on this repo** (46 format diffs, verified). Only `biome lint` is clean. If someone later formats the repo, CI can be upgraded to `biome ci .` — leave a one-line comment in the workflow noting this. -- **`bun install --frozen-lockfile`** — the publish workflow currently uses plain `bun install`; keep publish as-is (it must tolerate lockfile drift at release time is arguable, but out of scope), use frozen in CI so PRs can't silently change dependencies. -- **`typescript` is a devDependency and a peerDependency** — `bun install` installs it; `bunx tsc` resolves the local 6.x, not a global one. No extra setup needed. -- **The `types/` directory is gitignored?** Check: it is committed (exists in repo). `bunx tsc --noEmit` uses `tsconfig.json`, which *excludes* `types/` and `index.js` — no conflict with stale build artifacts. -- **Branch protection is not configurable from the repo** — after merging, the user must mark the `test` job as a required status check in GitHub settings for `main` and `next`. Note this in the PR description. - -## Acceptance criteria - -1. `ci.yml` exists; on a test PR, the `test` job passes all six steps and the `performance` job runs without being able to fail the overall check. -2. `npm-publish.yml` contains typecheck/lint/test/bundle-regression steps that execute before "Build package". -3. `act`-free validation: `bunx tsc --noEmit && bunx biome lint . && bun run test && bun test test/regression-bundle.test.ts && bun run build` exits 0 locally (this is exactly what the required job runs). -4. YAML is valid: `bunx yaml-lint .github/workflows/ci.yml` or simply GitHub's workflow parser accepting the file on push (no "workflow file issue" annotation). diff --git a/PLAN-package-exports.md b/PLAN-package-exports.md deleted file mode 100644 index c9d8337..0000000 --- a/PLAN-package-exports.md +++ /dev/null @@ -1,101 +0,0 @@ -# PLAN: Correct npm Packaging (exports map, tree-shaking, peer-dep metadata) - -## Goal - -Fix distribution metadata so the package resolves correctly and tree-shakes in all supported toolchains. Current problems in `package.json`: - -1. **No `exports` map** — no explicit entry conditions, no `types` condition, and deep imports into package internals are uncontrolled. -2. **`"module": "index.ts"` points at TypeScript source.** Bundlers that honor the `module` field (webpack, older Rollup configs) will try to parse raw TS from `node_modules`, which most consumer configs exclude from transpilation → build errors. `module` must point to JS. -3. **No `"sideEffects": false`** — REQUIREMENTS.md explicitly demands "the library must remain tree-shakable", but without this flag webpack retains the whole bundle. (Verified: the codebase has no module-level side effects — only const declarations, class definitions, and pure helpers; `Object.getPrototypeOf(async () => {})` in `src/util.ts` and the module-scope `WeakSet`/arrays in `graph.ts`/`slot.ts` are allocation-only, side-effect-free.) -4. **`main` is a *minified* bundle** (`index.js`, built with `bun build --minify`). Publishing minified code hurts consumer debugging and bug reports; consumers' bundlers minify anyway. The unminified `index.dev.js` exists but nothing points to it. -5. **`typescript` is a hard `peerDependency`** — JS-only consumers get an unresolvable-peer warning on every install. It should be optional. -6. **Packaging is controlled by a fragile `.npmignore`** with negation patterns; an explicit `files` allowlist is safer. - -## Files to touch - -- `package.json` -- `.npmignore` — delete (replaced by `files`) -- `test/regression-bundle.test.ts` — no change needed (it builds from `index.ts` on the fly, not from the shipped `index.js`); verify only. -- `REQUIREMENTS.md` — no change needed unless acceptance step 5 finds issues. - -## Implementation steps - -1. **Swap the roles of the two build outputs** so the published entry is the *unminified* ESM bundle. In `package.json` `scripts.build`, change: - - ``` - bunx tsc --project tsconfig.build.json && bun build index.ts --outdir ./ --minify && bun build index.ts --outfile index.dev.js - ``` - - to: - - ``` - bunx tsc --project tsconfig.build.json && bun build index.ts --outfile index.js - ``` - - and delete `index.dev.js` from the repo (`git rm index.dev.js`). The minified artifact serves no consumer (bundlers minify; the size budget is enforced by `test/regression-bundle.test.ts`, which builds its own minified bundle from source). If the user prefers to keep a minified file for CDN-style usage, instead keep both files and point a `"production"` exports condition at the minified one — but the default/simple path is one unminified `index.js`. - -2. **Rewrite the packaging fields in `package.json`:** - - ```json - "main": "index.js", - "types": "types/index.d.ts", - "exports": { - ".": { - "types": "./types/index.d.ts", - "bun": "./index.ts", - "default": "./index.js" - }, - "./package.json": "./package.json" - }, - "sideEffects": false, - "files": [ - "index.js", - "index.ts", - "src", - "types", - "SECURITY.md" - ], - ``` - - - **Delete the `"module": "index.ts"` field entirely.** With an `exports` map, `module` is ignored by modern resolvers; leaving it pointing at TS keeps the webpack hazard alive for tools that still read it. Do not repoint it at `index.js` — just remove it. - - The `"bun"` condition lets Bun consumers (Le Truc, per REQUIREMENTS.md) resolve the TS source directly — this is why `index.ts` and `src/` stay in `files`. - - `README.md` and `LICENSE` are always included by npm automatically; they do not need to be in `files`. - - Key order inside the `"."` conditions matters: `"types"` must come first, `"default"` last. - -3. **Make the TypeScript peer dependency optional:** - - ```json - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - ``` - -4. **Delete `.npmignore`.** With a `files` allowlist present, npm ignores `.npmignore` for inclusion logic anyway; keeping both invites drift. - -5. **Validate the package shape:** - - `bun run build` (regenerates `types/` and `index.js`). - - `npm pack --dry-run` — inspect the file list: exactly `index.js`, `index.ts`, `src/**`, `types/**`, `package.json`, `README.md`, `LICENSE`, `SECURITY.md`. No `test/`, no `adr/`, no `.agents/`, no `bench/`. - - `bunx publint` — must report no errors (warnings about `bun` condition ordering are acceptable if any). - - `bunx @arethetypeswrong/cli --pack .` — must show ESM resolution finding `types/index.d.ts` for the `import` condition; "no types for require" is expected and fine (the library is ESM-only by design; see the comment in `src/graph.ts:187–191` about live bindings — do NOT add a CJS build to satisfy the tool). - - Smoke test in a scratch dir: `npm pack`, then in `/tmp/pkgtest` create `package.json` (`"type": "module"`), `npm i `, and run `node -e "import('@zeix/cause-effect').then(m => console.log(typeof m.createState))"` → prints `function`. - -## Edge cases a weaker model would likely miss - -- **Do NOT add a CommonJS build.** `src/graph.ts` documents that `activeSink`/`activeOwner`/`batchDepth` are exported mutable `let` bindings relying on ESM live-binding semantics; a CJS transform snapshots them and silently breaks dependency tracking. ESM-only is a load-bearing design decision (REQUIREMENTS.md), not an oversight. -- **The `exports` map breaks deep imports** like `@zeix/cause-effect/src/nodes/list.ts`. Nothing in the repo or docs advertises deep imports, and the barrel re-exports everything, so this is acceptable — but mention it in the changelog entry (via the changelog-keeper skill) as a potentially breaking packaging change; consider a minor (not patch) version bump. -- **The `"bun"` condition must appear before `"default"`** or Bun will never reach it. Conditions are matched in object order. -- **`sideEffects: false` is a promise, not a flag** — if anyone later adds a module-level side effect (e.g., a global registration, a polyfill import), tree-shaking will silently drop it in consumer builds. Add a short comment in ARCHITECTURE.md or a note in the cause-effect-dev skill reference if the team wants a guardrail; at minimum, the core-tree-shaking regression test (`bundleCoreGzipped`) partially covers this. -- **`types/index.d.ts` is generated by `tsc --project tsconfig.build.json`** and the publish workflow runs `bun run build` before `npm publish` — so `files: ["types"]` is safe in CI. But `npm pack` from a fresh clone without building would produce a tarball missing fresh types; add `"prepack": "bun run build"`? **No** — npm runs `prepack` with npm's own node, and the build needs Bun. The publish workflow already builds; leave it, but note in the plan-executor's PR description that `npm pack` requires a prior `bun run build`. -- **`index.dev.js` removal**: grep the repo for references first (`grep -rn "index.dev" --exclude-dir=node_modules .`) — as of today it appears only in `package.json`'s build script and `.npmignore`; both are being edited by this plan. If it also appears in docs, update them. -- **The regression bundle test is independent of these changes** (it builds from `index.ts` with `Bun.build`), so size limits cannot regress from this plan — but run `bun run regression` anyway as a guard. - -## Acceptance criteria - -1. `npm pack --dry-run` lists exactly the intended files (see step 5) — no test/dev/tooling files. -2. `bunx publint` passes with no errors. -3. `bunx @arethetypeswrong/cli --pack .` shows correct ESM + types resolution (no ❌ for the `import` entrypoint; `require` failures acceptable/ESM-only). -4. Node smoke test (step 5, last bullet) prints `function`. -5. `package.json` contains `exports` (with `types` first), `sideEffects: false`, `files`, `peerDependenciesMeta.typescript.optional: true`, and no `module` field; `.npmignore` is deleted. -6. `bun test` and `bun run regression` still pass; `bun run build` produces `index.js` (unminified) and `types/`. diff --git a/biome.json b/biome.json index f8c0fd6..912700e 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**", "!index.js", "!index.dev.js", "!**/*.d.ts"] + "includes": ["**", "!index.js", "!**/*.d.ts"] }, "formatter": { "enabled": true, @@ -16,7 +16,7 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "preset": "recommended" } }, "javascript": { diff --git a/bun.lock b/bun.lock index 5ed9e48..1d5f1fb 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@zeix/cause-effect", "devDependencies": { - "@biomejs/biome": "2.4.6", + "@biomejs/biome": "^2.5.3", "@types/bun": "^1.3.14", "@zeix/cause-effect-stable": "npm:@zeix/cause-effect", "mitata": "^1.0.34", @@ -18,29 +18,29 @@ }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], + "@biomejs/biome": ["@biomejs/biome@2.5.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.3", "@biomejs/cli-darwin-x64": "2.5.3", "@biomejs/cli-linux-arm64": "2.5.3", "@biomejs/cli-linux-arm64-musl": "2.5.3", "@biomejs/cli-linux-x64": "2.5.3", "@biomejs/cli-linux-x64-musl": "2.5.3", "@biomejs/cli-win32-arm64": "2.5.3", "@biomejs/cli-win32-x64": "2.5.3" }, "bin": { "biome": "bin/biome" } }, "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@22.10.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww=="], - "@zeix/cause-effect-stable": ["@zeix/cause-effect@1.3.3", "", { "peerDependencies": { "typescript": ">=5.8.0" } }, "sha512-grXYA4hw1mQgIcQsl3qLwv/MFjLgfuGt7/0FPAA9U2sfy9BG3a1yjaSTKcySnVgimCNmzHWrwAKUJlxIg5ZS0A=="], + "@zeix/cause-effect-stable": ["@zeix/cause-effect@1.3.4", "", { "peerDependencies": { "typescript": ">=5.8.0" } }, "sha512-vQQ7YIV8TGwGy2xXsXuOyrt+XtnK2xbT4WdGrpa7KcebJRm+AFEF8a/i4ICg1vR1TQf8M3QG8s3/XTwGI0VR8Q=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], diff --git a/eslint.config.js b/eslint.config.js index 0d0e833..7904091 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint' export default [ // Global ignores to prevent warnings about these files { - ignores: ['index.js', 'index.dev.js', 'types/**/*.d.ts', '**/*.min.js'], + ignores: ['index.js', 'types/**/*.d.ts', '**/*.min.js'], }, { files: ['**/*.{js,mjs,cjs,ts}'], diff --git a/index.dev.js b/index.dev.js deleted file mode 100644 index eee51ae..0000000 --- a/index.dev.js +++ /dev/null @@ -1,1872 +0,0 @@ -// src/util.ts -var ASYNC_FUNCTION_PROTO = Object.getPrototypeOf(async () => {}); -function isFunction(fn) { - return typeof fn === "function"; -} -function isAsyncFunction(fn) { - return isFunction(fn) && Object.getPrototypeOf(fn) === ASYNC_FUNCTION_PROTO; -} -function isSyncFunction(fn) { - return isFunction(fn) && Object.getPrototypeOf(fn) !== ASYNC_FUNCTION_PROTO; -} -function isObjectOfType(value, type) { - return Object.prototype.toString.call(value) === `[object ${type}]`; -} -function isSignalOfType(value, type) { - return value != null && value[Symbol.toStringTag] === type; -} -function isRecord(value) { - return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype; -} -function valueString(value) { - if (typeof value === "string") - return `"${value}"`; - if (value != null && typeof value === "object") { - try { - return JSON.stringify(value); - } catch { - return String(value); - } - } - return String(value); -} - -// src/errors.ts -class CircularDependencyError extends Error { - constructor(where) { - super(`[${where}] Circular dependency detected`); - this.name = "CircularDependencyError"; - } -} - -class EffectConvergenceError extends Error { - constructor(passes) { - super(`[Effect] Effects did not settle after ${passes} flush passes — check for effects that write to signals they depend on`); - this.name = "EffectConvergenceError"; - } -} - -class NullishSignalValueError extends TypeError { - constructor(where) { - super(`[${where}] Signal value cannot be null or undefined`); - this.name = "NullishSignalValueError"; - } -} - -class UnsetSignalValueError extends Error { - constructor(where) { - super(`[${where}] Signal value is unset`); - this.name = "UnsetSignalValueError"; - } -} - -class InvalidSignalValueError extends TypeError { - constructor(where, value) { - super(`[${where}] Signal value ${valueString(value)} is invalid`); - this.name = "InvalidSignalValueError"; - } -} - -class InvalidCallbackError extends TypeError { - constructor(where, value) { - super(`[${where}] Callback ${valueString(value)} is invalid`); - this.name = "InvalidCallbackError"; - } -} - -class ReadonlySignalError extends Error { - constructor(where) { - super(`[${where}] Signal is read-only`); - this.name = "ReadonlySignalError"; - } -} - -class RequiredOwnerError extends Error { - constructor(where) { - super(`[${where}] Active owner is required`); - this.name = "RequiredOwnerError"; - } -} - -class PromiseValueError extends TypeError { - constructor(where) { - super(`[${where}] Callback returned a Promise — use an async callback to create a Task instead`); - this.name = "PromiseValueError"; - } -} - -class DuplicateKeyError extends Error { - constructor(where, key, value) { - super(`[${where}] Could not add key "${key}"${value != null ? ` with value ${JSON.stringify(value)}` : ""} because it already exists`); - this.name = "DuplicateKeyError"; - } -} - -class InvalidStoreMutationError extends TypeError { - constructor(prop, action) { - const guidance = action === "delete" ? `use store.remove(${JSON.stringify(prop)})` : `use store.${prop}.set(value), store.set(next), or store.add(key, value)`; - super(`[Store] Cannot ${action} property "${prop}" directly — ${guidance}`); - this.name = "InvalidStoreMutationError"; - } -} -function validateSignalValue(where, value, guard) { - if (value == null) - throw new NullishSignalValueError(where); - if (guard && !guard(value)) - throw new InvalidSignalValueError(where, value); -} -function validateReadValue(where, value) { - if (value == null) - throw new UnsetSignalValueError(where); -} -function validateCallback(where, value, guard = isFunction) { - if (!guard(value)) - throw new InvalidCallbackError(where, value); -} -// src/graph.ts -var TYPE_STATE = "State"; -var TYPE_MEMO = "Memo"; -var TYPE_TASK = "Task"; -var TYPE_SENSOR = "Sensor"; -var TYPE_LIST = "List"; -var TYPE_COLLECTION = "Collection"; -var TYPE_STORE = "Store"; -var TYPE_SLOT = "Slot"; -var FLAG_CLEAN = 0; -var FLAG_CHECK = 1 << 0; -var FLAG_DIRTY = 1 << 1; -var FLAG_RUNNING = 1 << 2; -var FLAG_RELINK = 1 << 3; -var activeSink = null; -var activeOwner = null; -var queuedEffects = []; -var batchDepth = 0; -var flushing = false; -var DEFAULT_EQUALITY = (a, b) => a === b; -var SKIP_EQUALITY = (_a, _b) => false; -var deepEqual = (a, b) => deepEqualInner(a, b, new WeakSet); -var deepEqualInner = (a, b, seen) => { - if (Object.is(a, b)) - return true; - if (typeof a !== typeof b) - return false; - if (a == null || typeof a !== "object" || b == null || typeof b !== "object") - return false; - if (seen.has(a)) - return true; - seen.add(a); - try { - const aIsArray = Array.isArray(a); - if (aIsArray !== Array.isArray(b)) - return false; - if (aIsArray) { - const aa = a; - const ba = b; - if (aa.length !== ba.length) - return false; - for (let i = 0;i < aa.length; i++) - if (!deepEqualInner(aa[i], ba[i], seen)) - return false; - return true; - } - if (a instanceof Date && b instanceof Date) - return a.getTime() === b.getTime(); - if (a instanceof RegExp && b instanceof RegExp) - return a.source === b.source && a.flags === b.flags; - if (isRecord(a) && isRecord(b)) { - const aKeys = Object.keys(a); - if (aKeys.length !== Object.keys(b).length) - return false; - for (const key of aKeys) { - if (!(key in b)) - return false; - if (!deepEqualInner(a[key], b[key], seen)) - return false; - } - return true; - } - return false; - } finally { - seen.delete(a); - } -}; -var DEEP_EQUALITY = (a, b) => deepEqual(a, b); -var isEqual = DEEP_EQUALITY; -function isValidEdge(checkEdge, node) { - const sourcesTail = node.sourcesTail; - if (sourcesTail) { - let edge = node.sources; - while (edge) { - if (edge === checkEdge) - return true; - if (edge === sourcesTail) - break; - edge = edge.nextSource; - } - } - return false; -} -function link(source, sink) { - const prevSource = sink.sourcesTail; - if (prevSource?.source === source) - return; - let nextSource = null; - const isRecomputing = sink.flags & FLAG_RUNNING; - if (isRecomputing) { - nextSource = prevSource ? prevSource.nextSource : sink.sources; - if (nextSource?.source === source) { - sink.sourcesTail = nextSource; - return; - } - } - const prevSink = source.sinksTail; - if (prevSink?.sink === sink && (!isRecomputing || isValidEdge(prevSink, sink))) - return; - const newEdge = { source, sink, nextSource, prevSink, nextSink: null }; - sink.sourcesTail = source.sinksTail = newEdge; - if (prevSource) - prevSource.nextSource = newEdge; - else - sink.sources = newEdge; - if (prevSink) - prevSink.nextSink = newEdge; - else - source.sinks = newEdge; -} -function unlink(edge) { - const { source, nextSource, nextSink, prevSink } = edge; - if (nextSink) - nextSink.prevSink = prevSink; - else - source.sinksTail = prevSink; - if (prevSink) - prevSink.nextSink = nextSink; - else - source.sinks = nextSink; - if (!source.sinks) { - if (source.stop) { - source.stop(); - source.stop = undefined; - } - if ("sources" in source && source.sources) { - const sinkNode = source; - sinkNode.sourcesTail = null; - trimSources(sinkNode); - sinkNode.flags |= FLAG_DIRTY; - } - } - return nextSource; -} -function trimSources(node) { - const tail = node.sourcesTail; - let source = tail ? tail.nextSource : node.sources; - while (source) - source = unlink(source); - if (tail) - tail.nextSource = null; - else - node.sources = null; -} -function propagate(node, newFlag = FLAG_DIRTY) { - const flags = node.flags; - if ("sinks" in node) { - if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) - return; - node.flags = flags | newFlag; - if ("controller" in node && node.controller) { - node.controller.abort(); - node.controller = undefined; - } - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink, FLAG_CHECK); - } else { - if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) - return; - const wasQueued = flags & (FLAG_DIRTY | FLAG_CHECK); - node.flags = flags & FLAG_RUNNING | newFlag; - if (!wasQueued) - queuedEffects.push(node); - } -} -function setState(node, next) { - if (node.equals(node.value, next)) - return; - node.value = next; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); -} -function registerCleanup(owner, fn) { - if (!owner.cleanup) - owner.cleanup = fn; - else if (Array.isArray(owner.cleanup)) - owner.cleanup.push(fn); - else - owner.cleanup = [owner.cleanup, fn]; -} -function runCleanup(owner) { - if (!owner.cleanup) - return; - if (Array.isArray(owner.cleanup)) - for (let i = 0;i < owner.cleanup.length; i++) - owner.cleanup[i](); - else - owner.cleanup(); - owner.cleanup = null; -} -function recomputeMemo(node) { - const prevWatcher = activeSink; - activeSink = node; - node.sourcesTail = null; - node.flags = FLAG_RUNNING; - let changed = false; - try { - const next = node.fn(node.value); - if (next instanceof Promise) - throw new PromiseValueError(TYPE_MEMO); - if (node.error || !node.equals(next, node.value)) { - node.value = next; - node.error = undefined; - changed = true; - } - } catch (err) { - changed = true; - node.error = err instanceof Error ? err : new Error(String(err)); - } finally { - activeSink = prevWatcher; - trimSources(node); - } - if (changed) { - for (let e = node.sinks;e; e = e.nextSink) - if (e.sink.flags & FLAG_CHECK) - e.sink.flags |= FLAG_DIRTY; - } - node.flags = FLAG_CLEAN; -} -function recomputeTask(node) { - node.controller?.abort(); - const controller = new AbortController; - node.controller = controller; - node.error = undefined; - const prevWatcher = activeSink; - activeSink = node; - node.sourcesTail = null; - node.flags = FLAG_RUNNING; - let promise; - try { - promise = node.fn(node.value, controller.signal); - } catch (err) { - node.controller = undefined; - node.error = err instanceof Error ? err : new Error(String(err)); - node.flags = FLAG_CLEAN; - setState(node.pendingNode, false); - return; - } finally { - activeSink = prevWatcher; - trimSources(node); - } - setState(node.pendingNode, true); - promise.then((next) => { - if (controller.signal.aborted) - return; - node.controller = undefined; - batch(() => { - if (node.error || !node.equals(next, node.value)) { - node.value = next; - node.error = undefined; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - } - setState(node.pendingNode, false); - }); - }, (err) => { - if (controller.signal.aborted) - return; - node.controller = undefined; - const error = err instanceof Error ? err : new Error(String(err)); - batch(() => { - if (!node.error || error.name !== node.error.name || error.message !== node.error.message) { - node.error = error; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - } - setState(node.pendingNode, false); - }); - }); - node.flags = FLAG_CLEAN; -} -function runEffect(node) { - runCleanup(node); - const prevContext = activeSink; - const prevOwner = activeOwner; - activeSink = activeOwner = node; - node.sourcesTail = null; - node.flags = FLAG_RUNNING; - try { - const out = node.fn(); - if (typeof out === "function") - registerCleanup(node, out); - } finally { - activeSink = prevContext; - activeOwner = prevOwner; - trimSources(node); - node.flags &= FLAG_DIRTY | FLAG_CHECK; - } -} -function refresh(node) { - if (node.flags & FLAG_CHECK) { - for (let e = node.sources;e; e = e.nextSource) { - if ("fn" in e.source) - refresh(e.source); - if (node.flags & FLAG_DIRTY) - break; - } - } - if (node.flags & FLAG_RUNNING) { - throw new CircularDependencyError("controller" in node ? TYPE_TASK : ("value" in node) ? TYPE_MEMO : "Effect"); - } - if (node.flags & FLAG_DIRTY) { - if ("controller" in node) - recomputeTask(node); - else if ("value" in node) - recomputeMemo(node); - else - runEffect(node); - } else { - node.flags = FLAG_CLEAN; - } -} -var MAX_FLUSH_PASSES = 1000; -function flush() { - if (flushing) - return; - flushing = true; - let errors; - let passes = 0; - try { - while (queuedEffects.length > 0) { - if (++passes > MAX_FLUSH_PASSES) { - queuedEffects.length = 0; - if (!errors) - errors = []; - errors.push(new EffectConvergenceError(MAX_FLUSH_PASSES)); - break; - } - const batch = queuedEffects.slice(); - queuedEffects.length = 0; - for (let i = 0;i < batch.length; i++) { - const effect = batch[i]; - if (effect.flags & FLAG_RUNNING) - continue; - if (effect.flags & (FLAG_DIRTY | FLAG_CHECK)) { - try { - refresh(effect); - } catch (err) { - if (!errors) - errors = []; - errors.push(err); - } - } - } - } - } finally { - flushing = false; - } - if (errors) { - if (errors.length === 1) - throw errors[0]; - throw new AggregateError(errors, "Multiple effects threw during flush"); - } -} -function scheduleEffect(node) { - if (node.flags & (FLAG_DIRTY | FLAG_CHECK)) { - queuedEffects.push(node); - if (batchDepth === 0) - flush(); - } -} -function batch(fn) { - batchDepth++; - try { - fn(); - } finally { - batchDepth--; - if (batchDepth === 0) - flush(); - } -} -function untrack(fn) { - const prev = activeSink; - activeSink = null; - try { - return fn(); - } finally { - activeSink = prev; - } -} -function createScope(fn, options) { - const prevOwner = activeOwner; - const scope = { cleanup: null }; - activeOwner = scope; - const dispose = () => runCleanup(scope); - try { - const out = fn(); - if (typeof out === "function") - registerCleanup(scope, out); - return dispose; - } finally { - activeOwner = prevOwner; - if (!options?.root && prevOwner) - registerCleanup(prevOwner, dispose); - } -} -function unown(fn) { - const prev = activeOwner; - activeOwner = null; - try { - return fn(); - } finally { - activeOwner = prev; - } -} -function makeSubscribe(node, onWatch) { - return onWatch ? () => { - if (activeSink) { - if (!node.sinks) - node.stop = onWatch(); - link(node, activeSink); - } - } : () => { - if (activeSink) - link(node, activeSink); - }; -} -// src/nodes/state.ts -function createState(value, options) { - validateSignalValue(TYPE_STATE, value, options?.guard); - const node = { - value, - sinks: null, - sinksTail: null, - equals: options?.equals ?? DEFAULT_EQUALITY, - guard: options?.guard - }; - return { - [Symbol.toStringTag]: TYPE_STATE, - get() { - if (activeSink) - link(node, activeSink); - return node.value; - }, - set(next) { - validateSignalValue(TYPE_STATE, next, node.guard); - setState(node, next); - }, - update(fn) { - validateCallback(TYPE_STATE, fn); - const next = fn(node.value); - validateSignalValue(TYPE_STATE, next, node.guard); - setState(node, next); - } - }; -} -function isState(value) { - return isSignalOfType(value, TYPE_STATE); -} - -// src/nodes/list.ts -function keysEqual(a, b) { - if (a.length !== b.length) - return false; - for (let i = 0;i < a.length; i++) - if (a[i] !== b[i]) - return false; - return true; -} -function getKeyGenerator(keyConfig) { - let keyCounter = 0; - const contentBased = typeof keyConfig === "function"; - return [ - typeof keyConfig === "string" ? () => `${keyConfig}${keyCounter++}` : contentBased ? (item) => keyConfig(item) || String(keyCounter++) : () => String(keyCounter++), - contentBased - ]; -} -function diffPositional(prev, next, prevKeys, generateKey, itemEquals) { - const add = {}; - const change = {}; - const remove = {}; - const nextKeys = []; - let changed = false; - const minLen = Math.min(prev.length, next.length); - for (let i = 0;i < minLen; i++) { - const key = prevKeys[i]; - nextKeys.push(key); - if (!itemEquals(prev[i], next[i])) { - change[key] = next[i]; - changed = true; - } - } - for (let i = minLen;i < next.length; i++) { - const val = next[i]; - const key = generateKey(val); - nextKeys.push(key); - add[key] = val; - changed = true; - } - for (let i = minLen;i < prev.length; i++) { - remove[prevKeys[i]] = null; - changed = true; - } - return { add, change, remove, newKeys: nextKeys, changed }; -} -function diffArrays(prev, next, prevKeys, generateKey, contentBased, itemEquals) { - if (!contentBased) - return diffPositional(prev, next, prevKeys, generateKey, itemEquals); - const add = {}; - const change = {}; - const remove = {}; - const nextKeys = []; - let changed = false; - const prevByKey = new Map; - for (let i = 0;i < prev.length; i++) { - const key = prevKeys[i]; - const item = prev[i]; - if (key && item !== undefined) - prevByKey.set(key, item); - } - const seenKeys = new Set; - for (let i = 0;i < next.length; i++) { - const val = next[i]; - validateSignalValue(`${TYPE_LIST} item at index ${i}`, val); - const key = generateKey(val); - if (seenKeys.has(key)) - throw new DuplicateKeyError(TYPE_LIST, key, val); - nextKeys.push(key); - seenKeys.add(key); - if (!prevByKey.has(key)) { - add[key] = val; - changed = true; - } else if (!itemEquals(prevByKey.get(key), val)) { - change[key] = val; - changed = true; - } - } - for (const [key] of prevByKey) { - if (!seenKeys.has(key)) { - remove[key] = null; - changed = true; - } - } - if (!changed && !keysEqual(prevKeys, nextKeys)) - changed = true; - return { add, change, remove, newKeys: nextKeys, changed }; -} -function createList(value, options) { - validateSignalValue(TYPE_LIST, value, Array.isArray); - const signals = new Map; - let keys = []; - const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig); - const itemEquals = options?.itemEquals ?? DEEP_EQUALITY; - const itemFactory = options?.createItem ?? ((item) => createState(item, { equals: itemEquals })); - const buildValue = () => { - const result = []; - for (const key of keys) { - const v = signals.get(key)?.get(); - if (v !== undefined) - result.push(v); - } - return result; - }; - const node = { - fn: buildValue, - value, - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - equals: DEEP_EQUALITY, - error: undefined - }; - const applyChanges = (changes) => { - let structural = false; - for (const key in changes.add) { - const val = changes.add[key]; - validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val); - signals.set(key, itemFactory(val)); - structural = true; - } - let hasChange = false; - for (const _key in changes.change) { - hasChange = true; - break; - } - if (hasChange) { - batch(() => { - for (const key in changes.change) { - const val = changes.change[key]; - validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val); - const signal = signals.get(key); - if (signal) - signal.set(val); - } - }); - } - for (const key in changes.remove) { - signals.delete(key); - const index = keys.indexOf(key); - if (index !== -1) - keys.splice(index, 1); - structural = true; - } - if (structural) - node.flags |= FLAG_RELINK; - return changes.changed; - }; - const subscribe = makeSubscribe(node, options?.watched); - for (let i = 0;i < value.length; i++) { - const val = value[i]; - if (val == null) - throw new NullishSignalValueError(`${TYPE_LIST} item ${i}`); - let key = keys[i]; - if (!key) { - key = generateKey(val); - keys[i] = key; - } - signals.set(key, itemFactory(val)); - } - node.value = value; - node.flags = 0; - const list = { - [Symbol.toStringTag]: TYPE_LIST, - [Symbol.isConcatSpreadable]: true, - *[Symbol.iterator]() { - subscribe(); - for (const key of keys) { - const signal = signals.get(key); - if (signal) - yield signal; - } - }, - get length() { - subscribe(); - return keys.length; - }, - get() { - subscribe(); - if (node.sources) { - if (node.flags) { - const relink = node.flags & FLAG_RELINK; - node.value = untrack(buildValue); - if (relink) { - node.flags = FLAG_DIRTY; - refresh(node); - if (node.error) - throw node.error; - } else { - node.flags = FLAG_CLEAN; - } - } - } else { - refresh(node); - if (node.error) - throw node.error; - } - return node.value; - }, - set(next) { - const prev = node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value; - const changes = diffArrays(prev, next, keys, generateKey, contentBased, itemEquals); - if (changes.changed) { - keys = changes.newKeys; - applyChanges(changes); - node.flags |= FLAG_DIRTY; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - } - }, - update(fn) { - list.set(fn(untrack(() => list.get()))); - }, - at(index) { - subscribe(); - const key = keys[index]; - return key !== undefined ? signals.get(key) : undefined; - }, - keys() { - subscribe(); - return keys.values(); - }, - byKey(key) { - subscribe(); - return signals.get(key); - }, - keyAt(index) { - subscribe(); - return keys[index]; - }, - indexOfKey(key) { - subscribe(); - return keys.indexOf(key); - }, - add(value2) { - const key = generateKey(value2); - if (signals.has(key)) - throw new DuplicateKeyError(TYPE_LIST, key, value2); - keys.push(key); - validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value2); - signals.set(key, itemFactory(value2)); - node.flags |= FLAG_DIRTY | FLAG_RELINK; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - return key; - }, - remove(keyOrIndex) { - const key = typeof keyOrIndex === "number" ? keys[keyOrIndex] : keyOrIndex; - if (key === undefined) - return; - const ok = signals.delete(key); - if (ok) { - const index = typeof keyOrIndex === "number" ? keyOrIndex : keys.indexOf(key); - if (index >= 0) - keys.splice(index, 1); - node.flags |= FLAG_DIRTY | FLAG_RELINK; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - } - }, - replace(key, value2) { - const signal = signals.get(key); - if (!signal) - return; - validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value2); - if (itemEquals(untrack(() => signal.get()), value2)) - return; - batch(() => { - signal.set(value2); - node.flags |= FLAG_DIRTY; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - }); - if (batchDepth === 0) - flush(); - }, - sort(compareFn) { - const entries = []; - untrack(() => { - for (const key of keys) { - const v = signals.get(key)?.get(); - if (v !== undefined) - entries.push([key, v]); - } - }); - entries.sort(isFunction(compareFn) ? (a, b) => compareFn(a[1], b[1]) : (a, b) => String(a[1]).localeCompare(String(b[1]))); - const newOrder = []; - for (const [key] of entries) - newOrder.push(key); - if (!keysEqual(keys, newOrder)) { - keys = newOrder; - node.flags |= FLAG_DIRTY; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - } - }, - splice(start, deleteCount, ...items) { - const length = keys.length; - const actualStart = start < 0 ? Math.max(0, length + start) : Math.min(start, length); - const actualDeleteCount = Math.max(0, Math.min(deleteCount ?? Math.max(0, length - Math.max(0, actualStart)), length - actualStart)); - const add = {}; - const remove = {}; - let hasRemove = false; - untrack(() => { - for (let i = 0;i < actualDeleteCount; i++) { - const index = actualStart + i; - const key = keys[index]; - if (key) { - const signal = signals.get(key); - if (signal) { - remove[key] = signal.get(); - hasRemove = true; - } - } - } - }); - const newOrder = keys.slice(0, actualStart); - const change = {}; - let hasAdd = false; - let hasChange = false; - for (const item of items) { - const key = generateKey(item); - if (key in remove) { - delete remove[key]; - change[key] = item; - hasChange = true; - } else if (signals.has(key)) { - throw new DuplicateKeyError(TYPE_LIST, key, item); - } else { - add[key] = item; - hasAdd = true; - } - newOrder.push(key); - } - newOrder.push(...keys.slice(actualStart + actualDeleteCount)); - const changed = hasAdd || hasRemove || hasChange; - if (changed) { - applyChanges({ - add, - change, - remove, - changed - }); - keys = newOrder; - node.flags |= FLAG_DIRTY; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - } - return Object.values(remove); - }, - deriveCollection(cb) { - return deriveCollection(list, cb); - } - }; - return list; -} -function isList(value) { - return isSignalOfType(value, TYPE_LIST); -} - -// src/nodes/memo.ts -function createMemo(fn, options) { - validateCallback(TYPE_MEMO, fn, isSyncFunction); - if (options?.value !== undefined) - validateSignalValue(TYPE_MEMO, options.value, options?.guard); - const node = { - fn, - value: options?.value, - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - equals: options?.equals ?? DEFAULT_EQUALITY, - error: undefined, - stop: undefined - }; - const watched = options?.watched; - const subscribe = makeSubscribe(node, watched ? () => watched(() => { - propagate(node); - if (batchDepth === 0) - flush(); - }) : undefined); - return { - [Symbol.toStringTag]: TYPE_MEMO, - get() { - subscribe(); - refresh(node); - if (node.error) - throw node.error; - validateReadValue(TYPE_MEMO, node.value); - return node.value; - } - }; -} -function isMemo(value) { - return isSignalOfType(value, TYPE_MEMO); -} - -// src/nodes/task.ts -function createTask(fn, options) { - validateCallback(TYPE_TASK, fn, isAsyncFunction); - if (options?.value !== undefined) - validateSignalValue(TYPE_TASK, options.value, options?.guard); - const pendingNode = { - value: false, - sinks: null, - sinksTail: null, - equals: DEFAULT_EQUALITY - }; - const node = { - fn, - value: options?.value, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - flags: FLAG_DIRTY, - equals: options?.equals ?? DEFAULT_EQUALITY, - controller: undefined, - error: undefined, - stop: undefined, - pendingNode - }; - const watched = options?.watched; - const subscribe = makeSubscribe(node, watched ? () => watched(() => { - propagate(node); - if (batchDepth === 0) - flush(); - }) : undefined); - const pendingSubscribe = makeSubscribe(pendingNode); - return { - [Symbol.toStringTag]: TYPE_TASK, - get() { - subscribe(); - refresh(node); - if (node.error) - throw node.error; - validateReadValue(TYPE_TASK, node.value); - return node.value; - }, - isPending() { - pendingSubscribe(); - return node.pendingNode.value; - }, - abort() { - node.controller?.abort(); - node.controller = undefined; - setState(node.pendingNode, false); - } - }; -} -function isTask(value) { - return isSignalOfType(value, TYPE_TASK); -} - -// src/nodes/collection.ts -function deriveCollection(source, callback) { - validateCallback(TYPE_COLLECTION, callback); - const isAsync = isAsyncFunction(callback); - const signals = new Map; - let keys = []; - const addSignal = (key) => { - const signal = isAsync ? createTask(async (prev, abort) => { - const itemSignal = untrack(() => source.byKey(key)); - if (!itemSignal) - return prev; - const sourceValue = itemSignal.get(); - if (sourceValue == null) - return prev; - return callback(sourceValue, abort); - }) : createMemo(() => { - const itemSignal = untrack(() => source.byKey(key)); - if (!itemSignal) - return; - const sourceValue = itemSignal.get(); - if (sourceValue == null) - return; - return callback(sourceValue); - }); - signals.set(key, signal); - }; - function syncKeys(nextKeys) { - if (!keysEqual(keys, nextKeys)) { - const nextSet = new Set(nextKeys); - for (const key of keys) - if (!nextSet.has(key)) - signals.delete(key); - for (const key of nextKeys) - if (!signals.has(key)) - addSignal(key); - keys = nextKeys; - node.flags |= FLAG_RELINK; - } - } - function buildValue() { - syncKeys(Array.from(source.keys())); - const result = []; - for (const key of keys) { - try { - const v = signals.get(key)?.get(); - if (v != null) - result.push(v); - } catch (e) { - if (!(e instanceof UnsetSignalValueError)) - throw e; - } - } - return result; - } - const valuesEqual = (a, b) => { - if (a.length !== b.length) - return false; - for (let i = 0;i < a.length; i++) - if (a[i] !== b[i]) - return false; - return true; - }; - const node = { - fn: buildValue, - value: [], - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - equals: valuesEqual, - error: undefined - }; - function ensureFresh() { - if (node.sources) { - if (node.flags) { - node.value = untrack(buildValue); - if (node.flags & FLAG_RELINK) { - node.flags = FLAG_DIRTY; - refresh(node); - if (node.error) - throw node.error; - } else { - node.flags = FLAG_CLEAN; - } - } - } else if (node.sinks) { - refresh(node); - if (node.error) - throw node.error; - } else { - node.value = untrack(buildValue); - } - } - const initialKeys = Array.from(untrack(() => source.keys())); - for (const key of initialKeys) - addSignal(key); - keys = initialKeys; - const collection = { - [Symbol.toStringTag]: TYPE_COLLECTION, - [Symbol.isConcatSpreadable]: true, - *[Symbol.iterator]() { - if (activeSink) - link(node, activeSink); - ensureFresh(); - for (const key of keys) { - const signal = signals.get(key); - if (signal) - yield signal; - } - }, - get length() { - if (activeSink) - link(node, activeSink); - ensureFresh(); - return keys.length; - }, - keys() { - if (activeSink) - link(node, activeSink); - ensureFresh(); - return keys.values(); - }, - get() { - if (activeSink) - link(node, activeSink); - ensureFresh(); - return node.value; - }, - at(index) { - if (activeSink) - link(node, activeSink); - ensureFresh(); - const key = keys[index]; - return key !== undefined ? signals.get(key) : undefined; - }, - byKey(key) { - if (activeSink) - link(node, activeSink); - ensureFresh(); - return signals.get(key); - }, - keyAt(index) { - if (activeSink) - link(node, activeSink); - ensureFresh(); - return keys[index]; - }, - indexOfKey(key) { - if (activeSink) - link(node, activeSink); - ensureFresh(); - return keys.indexOf(key); - }, - deriveCollection(cb) { - return deriveCollection(collection, cb); - } - }; - return collection; -} -function createCollection(watched, options) { - const value = options?.value ?? []; - if (value.length) - validateSignalValue(TYPE_COLLECTION, value, Array.isArray); - validateCallback(TYPE_COLLECTION, watched, isSyncFunction); - const signals = new Map; - const keys = []; - const itemToKey = new Map; - const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig); - const resolveKey = (item) => itemToKey.get(item) ?? (contentBased ? generateKey(item) : undefined); - const itemFactory = options?.createItem ?? ((item) => createState(item, { - equals: options?.itemEquals ?? DEEP_EQUALITY - })); - function buildValue() { - const result = []; - for (const key of keys) { - try { - const v = signals.get(key)?.get(); - if (v != null) - result.push(v); - } catch (e) { - if (!(e instanceof UnsetSignalValueError)) - throw e; - } - } - return result; - } - const node = { - fn: buildValue, - value, - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - equals: SKIP_EQUALITY, - error: undefined - }; - for (const item of value) { - const key = generateKey(item); - signals.set(key, itemFactory(item)); - itemToKey.set(item, key); - keys.push(key); - } - node.value = value; - node.flags = FLAG_DIRTY; - const onChanges = (changes) => { - const { add, change, remove } = changes; - if (!add?.length && !change?.length && !remove?.length) - return; - let structural = false; - batch(() => { - if (add) { - const staged = new Map; - for (const item of add) { - const key = generateKey(item); - if (signals.has(key) || staged.has(key)) - throw new DuplicateKeyError(TYPE_COLLECTION, key, item); - staged.set(key, item); - } - for (const [key, item] of staged) { - signals.set(key, itemFactory(item)); - itemToKey.set(item, key); - if (!keys.includes(key)) - keys.push(key); - structural = true; - } - } - if (change) { - for (const item of change) { - const key = resolveKey(item); - if (!key) - continue; - const signal = signals.get(key); - if (signal && isState(signal)) { - itemToKey.delete(untrack(() => signal.get())); - signal.set(item); - itemToKey.set(item, key); - } - } - } - if (remove) { - for (const item of remove) { - const key = resolveKey(item); - if (!key) - continue; - itemToKey.delete(item); - signals.delete(key); - const index = keys.indexOf(key); - if (index !== -1) - keys.splice(index, 1); - structural = true; - } - } - node.flags = FLAG_DIRTY | (structural ? FLAG_RELINK : 0); - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - }); - }; - const subscribe = makeSubscribe(node, () => watched(onChanges)); - const collection = { - [Symbol.toStringTag]: TYPE_COLLECTION, - [Symbol.isConcatSpreadable]: true, - *[Symbol.iterator]() { - subscribe(); - for (const key of keys) { - const signal = signals.get(key); - if (signal) - yield signal; - } - }, - get length() { - subscribe(); - return keys.length; - }, - keys() { - subscribe(); - return keys.values(); - }, - get() { - subscribe(); - if (node.sources) { - if (node.flags) { - const relink = node.flags & FLAG_RELINK; - node.value = untrack(buildValue); - if (relink) { - node.flags = FLAG_DIRTY; - refresh(node); - if (node.error) - throw node.error; - } else { - node.flags = FLAG_CLEAN; - } - } - } else { - refresh(node); - if (node.error) - throw node.error; - } - return node.value; - }, - at(index) { - subscribe(); - const key = keys[index]; - return key !== undefined ? signals.get(key) : undefined; - }, - byKey(key) { - subscribe(); - return signals.get(key); - }, - keyAt(index) { - subscribe(); - return keys[index]; - }, - indexOfKey(key) { - subscribe(); - return keys.indexOf(key); - }, - deriveCollection(cb) { - return deriveCollection(collection, cb); - } - }; - return collection; -} -function isCollection(value) { - return isSignalOfType(value, TYPE_COLLECTION); -} -// src/nodes/effect.ts -function createEffect(fn) { - validateCallback("Effect", fn); - const node = { - fn, - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - cleanup: null - }; - const dispose = () => { - runCleanup(node); - node.fn = undefined; - node.flags = FLAG_CLEAN; - node.sourcesTail = null; - trimSources(node); - }; - if (activeOwner) - registerCleanup(activeOwner, dispose); - runEffect(node); - scheduleEffect(node); - return dispose; -} -function match(signalOrSignals, handlers) { - if (!activeOwner) - throw new RequiredOwnerError("match"); - const isSingle = !Array.isArray(signalOrSignals); - const signals = isSingle ? [signalOrSignals] : signalOrSignals; - const { nil, stale } = handlers; - const ok = isSingle ? (values2) => handlers.ok(values2[0]) : (values2) => handlers.ok(values2); - const err = isSingle && handlers.err ? (errors2) => handlers.err(errors2[0]) : handlers.err ?? console.error; - let errors; - let pending = false; - const values = new Array(signals.length); - for (let i = 0;i < signals.length; i++) { - try { - values[i] = signals[i].get(); - } catch (e) { - if (e instanceof UnsetSignalValueError) { - pending = true; - continue; - } - if (!errors) - errors = []; - errors.push(e instanceof Error ? e : new Error(String(e))); - } - } - let out; - try { - if (pending) - out = nil?.(); - else if (errors) - out = err(errors); - else if (stale && (isSingle ? isTask(signals[0]) && signals[0].isPending() : signals.some((s) => isTask(s) && s.isPending()))) - out = stale(); - else - out = ok(values); - } catch (e) { - out = err([e instanceof Error ? e : new Error(String(e))]); - } - if (typeof out === "function") - return out; - if (out instanceof Promise) { - const owner = activeOwner; - const controller = new AbortController; - registerCleanup(owner, () => controller.abort()); - out.then((cleanup) => { - if (!controller.signal.aborted && typeof cleanup === "function") - registerCleanup(owner, cleanup); - }).catch((e) => { - err([e instanceof Error ? e : new Error(String(e))]); - }); - } -} -// src/nodes/sensor.ts -function createSensor(watched, options) { - validateCallback(TYPE_SENSOR, watched, isSyncFunction); - if (options?.value !== undefined) - validateSignalValue(TYPE_SENSOR, options.value, options?.guard); - const node = { - value: options?.value, - sinks: null, - sinksTail: null, - equals: options?.equals ?? DEFAULT_EQUALITY, - guard: options?.guard, - stop: undefined - }; - return { - [Symbol.toStringTag]: TYPE_SENSOR, - get() { - if (activeSink) { - if (!node.sinks) - node.stop = watched((next) => { - validateSignalValue(TYPE_SENSOR, next, node.guard); - setState(node, next); - }); - link(node, activeSink); - } - validateReadValue(TYPE_SENSOR, node.value); - return node.value; - } - }; -} -function isSensor(value) { - return isSignalOfType(value, TYPE_SENSOR); -} -// src/nodes/store.ts -function diffRecords(prev, next) { - const add = {}; - const change = {}; - const remove = {}; - let changed = false; - const prevKeys = Object.keys(prev); - const nextKeys = Object.keys(next); - for (const key of nextKeys) { - if (key in prev) { - if (!DEEP_EQUALITY(prev[key], next[key])) { - change[key] = next[key]; - changed = true; - } - } else { - add[key] = next[key]; - changed = true; - } - } - for (const key of prevKeys) { - if (!(key in next)) { - remove[key] = undefined; - changed = true; - } - } - return { add, change, remove, changed }; -} -function createStore(value, options) { - validateSignalValue(TYPE_STORE, value, isRecord); - const signals = new Map; - const addSignal = (key, val) => { - validateSignalValue(`${TYPE_STORE} for key "${key}"`, val); - if (Array.isArray(val)) - signals.set(key, createList(val)); - else if (isRecord(val)) - signals.set(key, createStore(val)); - else - signals.set(key, createState(val)); - }; - const shapeCategory = (val) => { - if (Array.isArray(val)) - return "list"; - if (isRecord(val)) - return "store"; - return "state"; - }; - const signalCategory = (signal) => { - if (isList(signal)) - return "list"; - if (isStore(signal)) - return "store"; - return "state"; - }; - const buildValue = () => { - const record = {}; - for (const [key, signal] of signals) - record[key] = signal.get(); - return record; - }; - const node = { - fn: buildValue, - value, - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - equals: DEEP_EQUALITY, - error: undefined - }; - const applyChanges = (changes) => { - let structural = false; - for (const key in changes.add) { - addSignal(key, changes.add[key]); - structural = true; - } - let hasChange = false; - for (const _key in changes.change) { - hasChange = true; - break; - } - if (hasChange) { - batch(() => { - for (const key in changes.change) { - const val = changes.change[key]; - validateSignalValue(`${TYPE_STORE} for key "${key}"`, val); - const signal = signals.get(key); - if (signal) { - if (shapeCategory(val) !== signalCategory(signal)) { - addSignal(key, val); - structural = true; - } else - signal.set(val); - } - } - }); - } - for (const key in changes.remove) { - signals.delete(key); - structural = true; - } - if (structural) - node.flags |= FLAG_RELINK; - return changes.changed; - }; - const subscribe = makeSubscribe(node, options?.watched); - for (const key of Object.keys(value)) - addSignal(key, value[key]); - const store = { - [Symbol.toStringTag]: TYPE_STORE, - [Symbol.isConcatSpreadable]: false, - *[Symbol.iterator]() { - subscribe(); - for (const [key, signal] of signals) { - yield [key, signal]; - } - }, - keys() { - subscribe(); - return signals.keys(); - }, - byKey(key) { - return signals.get(key); - }, - get() { - subscribe(); - if (node.sources) { - if (node.flags) { - const relink = node.flags & FLAG_RELINK; - node.value = untrack(buildValue); - if (relink) { - node.flags = FLAG_DIRTY; - refresh(node); - if (node.error) - throw node.error; - } else { - node.flags = FLAG_CLEAN; - } - } - } else { - refresh(node); - if (node.error) - throw node.error; - } - return node.value; - }, - set(next) { - const prev = node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value; - const changes = diffRecords(prev, next); - if (applyChanges(changes)) { - node.flags |= FLAG_DIRTY; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - } - }, - update(fn) { - store.set(fn(untrack(() => store.get()))); - }, - add(key, value2) { - if (signals.has(key)) - throw new DuplicateKeyError(TYPE_STORE, key, value2); - addSignal(key, value2); - node.flags |= FLAG_DIRTY | FLAG_RELINK; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - return key; - }, - remove(key) { - const ok = signals.delete(key); - if (ok) { - node.flags |= FLAG_DIRTY | FLAG_RELINK; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - } - } - }; - return new Proxy(store, { - get(target, prop) { - if (prop in target) - return Reflect.get(target, prop); - if (typeof prop !== "symbol") - return target.byKey(prop); - }, - set(_target, prop) { - throw new InvalidStoreMutationError(String(prop), "assign to"); - }, - deleteProperty(_target, prop) { - throw new InvalidStoreMutationError(String(prop), "delete"); - }, - defineProperty(_target, prop) { - throw new InvalidStoreMutationError(String(prop), "define"); - }, - has(target, prop) { - if (prop in target) - return true; - return target.byKey(String(prop)) !== undefined; - }, - ownKeys(target) { - return Array.from(target.keys()); - }, - getOwnPropertyDescriptor(target, prop) { - if (prop in target) - return Reflect.getOwnPropertyDescriptor(target, prop); - if (typeof prop === "symbol") - return; - const signal = target.byKey(String(prop)); - return signal ? { - enumerable: true, - configurable: true, - writable: true, - value: signal - } : undefined; - } - }); -} -function isStore(value) { - return isSignalOfType(value, TYPE_STORE); -} - -// src/signal.ts -var SIGNAL_TYPES = new Set([ - TYPE_STATE, - TYPE_MEMO, - TYPE_TASK, - TYPE_SENSOR, - TYPE_SLOT, - TYPE_LIST, - TYPE_COLLECTION, - TYPE_STORE -]); -function createComputed(callback, options) { - return isAsyncFunction(callback) ? createTask(callback, options) : createMemo(callback, options); -} -function createSignal(value) { - if (isSignal(value)) - return value; - if (value == null) - throw new InvalidSignalValueError("createSignal", value); - if (isAsyncFunction(value)) - return createTask(value); - if (isFunction(value)) - return createMemo(value); - if (Array.isArray(value) && value.every((item) => item != null)) - return createList(value); - if (isRecord(value)) - return createStore(value); - return createState(value); -} -function createMutableSignal(value) { - if (isMutableSignal(value)) - return value; - if (value == null || isFunction(value) || isSignal(value)) - throw new InvalidSignalValueError("createMutableSignal", value); - if (Array.isArray(value) && value.every((item) => item != null)) - return createList(value); - if (isRecord(value)) - return createStore(value); - return createState(value); -} -function isComputed(value) { - return isMemo(value) || isTask(value); -} -function isSignal(value) { - return value != null && SIGNAL_TYPES.has(value[Symbol.toStringTag]); -} -function isMutableSignal(value) { - return isState(value) || isStore(value) || isList(value); -} - -// src/nodes/slot.ts -var settingSlots = new WeakSet; -function isSignalOrDescriptor(value) { - if (isSignal(value)) - return true; - return value !== null && typeof value === "object" && "get" in value && typeof value.get === "function"; -} -function createSlot(initialSignal, options) { - validateSignalValue(TYPE_SLOT, initialSignal, isSignalOrDescriptor); - let delegated = initialSignal; - const guard = options?.guard; - const node = { - fn: () => delegated.get(), - value: undefined, - flags: FLAG_DIRTY, - sources: null, - sourcesTail: null, - sinks: null, - sinksTail: null, - equals: options?.equals ?? DEFAULT_EQUALITY, - error: undefined - }; - const get = () => { - if (activeSink) - link(node, activeSink); - refresh(node); - if (node.error) - throw node.error; - return node.value; - }; - const set = (next) => { - if (settingSlots.has(node)) - throw new Error("[Slot] Circular delegation detected in set()"); - settingSlots.add(node); - try { - if (isSlot(delegated)) - return void delegated.set(next); - if ("set" in delegated && typeof delegated.set === "function") { - validateSignalValue(TYPE_SLOT, next, guard); - delegated.set(next); - } else { - throw new ReadonlySignalError(TYPE_SLOT); - } - } finally { - settingSlots.delete(node); - } - }; - const replace = (next) => { - validateSignalValue(TYPE_SLOT, next, isSignalOrDescriptor); - delegated = next; - node.flags |= FLAG_DIRTY; - for (let e = node.sinks;e; e = e.nextSink) - propagate(e.sink); - if (batchDepth === 0) - flush(); - }; - return { - [Symbol.toStringTag]: TYPE_SLOT, - configurable: true, - enumerable: true, - get, - set, - replace, - current: () => delegated - }; -} -function isSlot(value) { - return isSignalOfType(value, TYPE_SLOT); -} -export { - valueString, - untrack, - unown, - match, - isTask, - isStore, - isState, - isSlot, - isSignalOfType, - isSignal, - isSensor, - isRecord, - isObjectOfType, - isMutableSignal, - isMemo, - isList, - isFunction, - isEqual, - isComputed, - isCollection, - isAsyncFunction, - createTask, - createStore, - createState, - createSlot, - createSignal, - createSensor, - createScope, - createMutableSignal, - createMemo, - createList, - createEffect, - createComputed, - createCollection, - batch, - UnsetSignalValueError, - SKIP_EQUALITY, - RequiredOwnerError, - ReadonlySignalError, - PromiseValueError, - NullishSignalValueError, - InvalidStoreMutationError, - InvalidSignalValueError, - InvalidCallbackError, - EffectConvergenceError, - DuplicateKeyError, - DEFAULT_EQUALITY, - DEEP_EQUALITY, - CircularDependencyError -}; diff --git a/index.js b/index.js index 482ef63..eee51ae 100644 --- a/index.js +++ b/index.js @@ -1 +1,1872 @@ -var iz=Object.getPrototypeOf(async()=>{});function c(z){return typeof z==="function"}function Zz(z){return c(z)&&Object.getPrototypeOf(z)===iz}function Bz(z){return c(z)&&Object.getPrototypeOf(z)!==iz}function az(z,J){return Object.prototype.toString.call(z)===`[object ${J}]`}function f(z,J){return z!=null&&z[Symbol.toStringTag]===J}function k(z){return z!==null&&typeof z==="object"&&Object.getPrototypeOf(z)===Object.prototype}function xz(z){if(typeof z==="string")return`"${z}"`;if(z!=null&&typeof z==="object")try{return JSON.stringify(z)}catch{return String(z)}return String(z)}class Cz extends Error{constructor(z){super(`[${z}] Circular dependency detected`);this.name="CircularDependencyError"}}class Fz extends Error{constructor(z){super(`[Effect] Effects did not settle after ${z} flush passes — check for effects that write to signals they depend on`);this.name="EffectConvergenceError"}}class Pz extends TypeError{constructor(z){super(`[${z}] Signal value cannot be null or undefined`);this.name="NullishSignalValueError"}}class $z extends Error{constructor(z){super(`[${z}] Signal value is unset`);this.name="UnsetSignalValueError"}}class Hz extends TypeError{constructor(z,J){super(`[${z}] Signal value ${xz(J)} is invalid`);this.name="InvalidSignalValueError"}}class Tz extends TypeError{constructor(z,J){super(`[${z}] Callback ${xz(J)} is invalid`);this.name="InvalidCallbackError"}}class Yz extends Error{constructor(z){super(`[${z}] Signal is read-only`);this.name="ReadonlySignalError"}}class Iz extends Error{constructor(z){super(`[${z}] Active owner is required`);this.name="RequiredOwnerError"}}class mz extends TypeError{constructor(z){super(`[${z}] Callback returned a Promise — use an async callback to create a Task instead`);this.name="PromiseValueError"}}class d extends Error{constructor(z,J,Z){super(`[${z}] Could not add key "${J}"${Z!=null?` with value ${JSON.stringify(Z)}`:""} because it already exists`);this.name="DuplicateKeyError"}}class Qz extends TypeError{constructor(z,J){let Z=J==="delete"?`use store.remove(${JSON.stringify(z)})`:`use store.${z}.set(value), store.set(next), or store.add(key, value)`;super(`[Store] Cannot ${J} property "${z}" directly — ${Z}`);this.name="InvalidStoreMutationError"}}function O(z,J,Z){if(J==null)throw new Pz(z);if(Z&&!Z(J))throw new Hz(z,J)}function qz(z,J){if(J==null)throw new $z(z)}function h(z,J,Z=c){if(!Z(J))throw new Tz(z,J)}var n="State",l="Memo",a="Task",e="Sensor",L="List",i="Collection",zz="Store",Jz="Slot",S=0,r=1,N=2,Xz=4,b=8,G=null,T=null,Mz=[],x=0,Ez=!1,p=(z,J)=>z===J,Sz=(z,J)=>!1,ez=(z,J)=>hz(z,J,new WeakSet),hz=(z,J,Z)=>{if(Object.is(z,J))return!0;if(typeof z!==typeof J)return!1;if(z==null||typeof z!=="object"||J==null||typeof J!=="object")return!1;if(Z.has(z))return!0;Z.add(z);try{let $=Array.isArray(z);if($!==Array.isArray(J))return!1;if($){let B=z,U=J;if(B.length!==U.length)return!1;for(let D=0;Dez(z,J),zJ=s;function JJ(z,J){let Z=J.sourcesTail;if(Z){let $=J.sources;while($){if($===z)return!0;if($===Z)break;$=$.nextSource}}return!1}function _(z,J){let Z=J.sourcesTail;if(Z?.source===z)return;let $=null,B=J.flags&Xz;if(B){if($=Z?Z.nextSource:J.sources,$?.source===z){J.sourcesTail=$;return}}let U=z.sinksTail;if(U?.sink===J&&(!B||JJ(U,J)))return;let D={source:z,sink:J,nextSource:$,prevSink:U,nextSink:null};if(J.sourcesTail=z.sinksTail=D,Z)Z.nextSource=D;else J.sources=D;if(U)U.nextSink=D;else z.sinks=D}function ZJ(z){let{source:J,nextSource:Z,nextSink:$,prevSink:B}=z;if($)$.prevSink=B;else J.sinksTail=B;if(B)B.nextSink=$;else J.sinks=$;if(!J.sinks){if(J.stop)J.stop(),J.stop=void 0;if("sources"in J&&J.sources){let U=J;U.sourcesTail=null,Uz(U),U.flags|=N}}return Z}function Uz(z){let J=z.sourcesTail,Z=J?J.nextSource:z.sources;while(Z)Z=ZJ(Z);if(J)J.nextSource=null;else z.sources=null}function w(z,J=N){let Z=z.flags;if("sinks"in z){if((Z&(N|r))>=J)return;if(z.flags=Z|J,"controller"in z&&z.controller)z.controller.abort(),z.controller=void 0;for(let $=z.sinks;$;$=$.nextSink)w($.sink,r)}else{if((Z&(N|r))>=J)return;let $=Z&(N|r);if(z.flags=Z&Xz|J,!$)Mz.push(z)}}function g(z,J){if(z.equals(z.value,J))return;z.value=J;for(let Z=z.sinks;Z;Z=Z.nextSink)w(Z.sink);if(x===0)F()}function jz(z,J){if(!z.cleanup)z.cleanup=J;else if(Array.isArray(z.cleanup))z.cleanup.push(J);else z.cleanup=[z.cleanup,J]}function Az(z){if(!z.cleanup)return;if(Array.isArray(z.cleanup))for(let J=0;J{if(J.signal.aborted)return;z.controller=void 0,u(()=>{if(z.error||!z.equals(B,z.value)){z.value=B,z.error=void 0;for(let U=z.sinks;U;U=U.nextSink)w(U.sink)}g(z.pendingNode,!1)})},(B)=>{if(J.signal.aborted)return;z.controller=void 0;let U=B instanceof Error?B:Error(String(B));u(()=>{if(!z.error||U.name!==z.error.name||U.message!==z.error.message){z.error=U;for(let D=z.sinks;D;D=D.nextSink)w(D.sink)}g(z.pendingNode,!1)})}),z.flags=S}function yz(z){Az(z);let J=G,Z=T;G=T=z,z.sourcesTail=null,z.flags=Xz;try{let $=z.fn();if(typeof $==="function")jz(z,$)}finally{G=J,T=Z,Uz(z),z.flags&=N|r}}function A(z){if(z.flags&r)for(let J=z.sources;J;J=J.nextSource){if("fn"in J.source)A(J.source);if(z.flags&N)break}if(z.flags&Xz)throw new Cz("controller"in z?a:("value"in z)?l:"Effect");if(z.flags&N)if("controller"in z)jJ(z);else if("value"in z)$J(z);else yz(z);else z.flags=S}var sz=1000;function F(){if(Ez)return;Ez=!0;let z,J=0;try{while(Mz.length>0){if(++J>sz){if(Mz.length=0,!z)z=[];z.push(new Fz(sz));break}let Z=Mz.slice();Mz.length=0;for(let $=0;$Az($);try{let U=z();if(typeof U==="function")jz($,U);return B}finally{if(T=Z,!J?.root&&Z)jz(Z,B)}}function WJ(z){let J=T;T=null;try{return z()}finally{T=J}}function v(z,J){return J?()=>{if(G){if(!z.sinks)z.stop=J();_(z,G)}}:()=>{if(G)_(z,G)}}function t(z,J){O(n,z,J?.guard);let Z={value:z,sinks:null,sinksTail:null,equals:J?.equals??p,guard:J?.guard};return{[Symbol.toStringTag]:n,get(){if(G)_(Z,G);return Z.value},set($){O(n,$,Z.guard),g(Z,$)},update($){h(n,$);let B=$(Z.value);O(n,B,Z.guard),g(Z,B)}}}function Rz(z){return f(z,n)}function fz(z,J){if(z.length!==J.length)return!1;for(let Z=0;Z`${z}${J++}`:Z?($)=>z($)||String(J++):()=>String(J++),Z]}function BJ(z,J,Z,$,B){let U={},D={},V={},P=[],q=!1,K=Math.min(z.length,J.length);for(let X=0;Xt(j,{equals:D})),P=()=>{let j=[];for(let H of $){let W=Z.get(H)?.get();if(W!==void 0)j.push(W)}return j},q={fn:P,value:z,flags:N,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:s,error:void 0},K=(j)=>{let H=!1;for(let M in j.add){let R=j.add[M];O(`${L} item for key "${M}"`,R),Z.set(M,V(R)),H=!0}let W=!1;for(let M in j.change){W=!0;break}if(W)u(()=>{for(let M in j.change){let R=j.change[M];O(`${L} item for key "${M}"`,R);let y=Z.get(M);if(y)y.set(R)}});for(let M in j.remove){Z.delete(M);let R=$.indexOf(M);if(R!==-1)$.splice(R,1);H=!0}if(H)q.flags|=b;return j.changed},X=v(q,J?.watched);for(let j=0;jQ.get())))},at(j){X();let H=$[j];return H!==void 0?Z.get(H):void 0},keys(){return X(),$.values()},byKey(j){return X(),Z.get(j)},keyAt(j){return X(),$[j]},indexOfKey(j){return X(),$.indexOf(j)},add(j){let H=B(j);if(Z.has(H))throw new d(L,H,j);$.push(H),O(`${L} item for key "${H}"`,j),Z.set(H,V(j)),q.flags|=N|b;for(let W=q.sinks;W;W=W.nextSink)w(W.sink);if(x===0)F();return H},remove(j){let H=typeof j==="number"?$[j]:j;if(H===void 0)return;if(Z.delete(H)){let M=typeof j==="number"?j:$.indexOf(H);if(M>=0)$.splice(M,1);q.flags|=N|b;for(let R=q.sinks;R;R=R.nextSink)w(R.sink);if(x===0)F()}},replace(j,H){let W=Z.get(j);if(!W)return;if(O(`${L} item for key "${j}"`,H),D(Y(()=>W.get()),H))return;if(u(()=>{W.set(H),q.flags|=N;for(let M=q.sinks;M;M=M.nextSink)w(M.sink)}),x===0)F()},sort(j){let H=[];Y(()=>{for(let M of $){let R=Z.get(M)?.get();if(R!==void 0)H.push([M,R])}}),H.sort(c(j)?(M,R)=>j(M[1],R[1]):(M,R)=>String(M[1]).localeCompare(String(R[1])));let W=[];for(let[M]of H)W.push(M);if(!fz($,W)){$=W,q.flags|=N;for(let M=q.sinks;M;M=M.nextSink)w(M.sink);if(x===0)F()}},splice(j,H,...W){let M=$.length,R=j<0?Math.max(0,M+j):Math.min(j,M),y=Math.max(0,Math.min(H??Math.max(0,M-Math.max(0,R)),M-R)),Wz={},C={},I=!1;Y(()=>{for(let E=0;E$(()=>{if(w(Z),x===0)F()}):void 0);return{[Symbol.toStringTag]:l,get(){if(B(),A(Z),Z.error)throw Z.error;return qz(l,Z.value),Z.value}}}function kz(z){return f(z,l)}function Dz(z,J){if(h(a,z,Zz),J?.value!==void 0)O(a,J.value,J?.guard);let Z={value:!1,sinks:null,sinksTail:null,equals:p},$={fn:z,value:J?.value,sources:null,sourcesTail:null,sinks:null,sinksTail:null,flags:N,equals:J?.equals??p,controller:void 0,error:void 0,stop:void 0,pendingNode:Z},B=J?.watched,U=v($,B?()=>B(()=>{if(w($),x===0)F()}):void 0),D=v(Z);return{[Symbol.toStringTag]:a,get(){if(U(),A($),$.error)throw $.error;return qz(a,$.value),$.value},isPending(){return D(),$.pendingNode.value},abort(){$.controller?.abort(),$.controller=void 0,g($.pendingNode,!1)}}}function Gz(z){return f(z,a)}function _z(z,J){h(i,J);let Z=Zz(J),$=new Map,B=[],U=(j)=>{let H=Z?Dz(async(W,M)=>{let R=Y(()=>z.byKey(j));if(!R)return W;let y=R.get();if(y==null)return W;return J(y,M)}):Nz(()=>{let W=Y(()=>z.byKey(j));if(!W)return;let M=W.get();if(M==null)return;return J(M)});$.set(j,H)};function D(j){if(!fz(B,j)){let H=new Set(j);for(let W of B)if(!H.has(W))$.delete(W);for(let W of j)if(!$.has(W))U(W);B=j,q.flags|=b}}function V(){D(Array.from(z.keys()));let j=[];for(let H of B)try{let W=$.get(H)?.get();if(W!=null)j.push(W)}catch(W){if(!(W instanceof $z))throw W}return j}let q={fn:V,value:[],flags:N,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:(j,H)=>{if(j.length!==H.length)return!1;for(let W=0;Wz.keys()));for(let j of X)U(j);B=X;let Q={[Symbol.toStringTag]:i,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){if(G)_(q,G);K();for(let j of B){let H=$.get(j);if(H)yield H}},get length(){if(G)_(q,G);return K(),B.length},keys(){if(G)_(q,G);return K(),B.values()},get(){if(G)_(q,G);return K(),q.value},at(j){if(G)_(q,G);K();let H=B[j];return H!==void 0?$.get(H):void 0},byKey(j){if(G)_(q,G);return K(),$.get(j)},keyAt(j){if(G)_(q,G);return K(),B[j]},indexOfKey(j){if(G)_(q,G);return K(),B.indexOf(j)},deriveCollection(j){return _z(Q,j)}};return Q}function QJ(z,J){let Z=J?.value??[];if(Z.length)O(i,Z,Array.isArray);h(i,z,Bz);let $=new Map,B=[],U=new Map,[D,V]=pz(J?.keyConfig),P=(W)=>U.get(W)??(V?D(W):void 0),q=J?.createItem??((W)=>t(W,{equals:J?.itemEquals??s}));function K(){let W=[];for(let M of B)try{let R=$.get(M)?.get();if(R!=null)W.push(R)}catch(R){if(!(R instanceof $z))throw R}return W}let X={fn:K,value:Z,flags:N,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:Sz,error:void 0};for(let W of Z){let M=D(W);$.set(M,q(W)),U.set(W,M),B.push(M)}X.value=Z,X.flags=N;let Q=(W)=>{let{add:M,change:R,remove:y}=W;if(!M?.length&&!R?.length&&!y?.length)return;let Wz=!1;u(()=>{if(M){let C=new Map;for(let I of M){let m=D(I);if($.has(m)||C.has(m))throw new d(i,m,I);C.set(m,I)}for(let[I,m]of C){if($.set(I,q(m)),U.set(m,I),!B.includes(I))B.push(I);Wz=!0}}if(R)for(let C of R){let I=P(C);if(!I)continue;let m=$.get(I);if(m&&Rz(m))U.delete(Y(()=>m.get())),m.set(C),U.set(C,I)}if(y)for(let C of y){let I=P(C);if(!I)continue;U.delete(C),$.delete(I);let m=B.indexOf(I);if(m!==-1)B.splice(m,1);Wz=!0}X.flags=N|(Wz?b:0);for(let C=X.sinks;C;C=C.nextSink)w(C.sink)})},j=v(X,()=>z(Q)),H={[Symbol.toStringTag]:i,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){j();for(let W of B){let M=$.get(W);if(M)yield M}},get length(){return j(),B.length},keys(){return j(),B.values()},get(){if(j(),X.sources){if(X.flags){let W=X.flags&b;if(X.value=Y(K),W){if(X.flags=N,A(X),X.error)throw X.error}else X.flags=S}}else if(A(X),X.error)throw X.error;return X.value},at(W){j();let M=B[W];return M!==void 0?$.get(M):void 0},byKey(W){return j(),$.get(W)},keyAt(W){return j(),B[W]},indexOfKey(W){return j(),B.indexOf(W)},deriveCollection(W){return _z(H,W)}};return H}function qJ(z){return f(z,i)}function MJ(z){h("Effect",z);let J={fn:z,flags:N,sources:null,sourcesTail:null,cleanup:null},Z=()=>{Az(J),J.fn=void 0,J.flags=S,J.sourcesTail=null,Uz(J)};if(T)jz(T,Z);return yz(J),tz(J),Z}function UJ(z,J){if(!T)throw new Iz("match");let Z=!Array.isArray(z),$=Z?[z]:z,{nil:B,stale:U}=J,D=Z?(Q)=>J.ok(Q[0]):(Q)=>J.ok(Q),V=Z&&J.err?(Q)=>J.err(Q[0]):J.err??console.error,P,q=!1,K=Array($.length);for(let Q=0;Q<$.length;Q++)try{K[Q]=$[Q].get()}catch(j){if(j instanceof $z){q=!0;continue}if(!P)P=[];P.push(j instanceof Error?j:Error(String(j)))}let X;try{if(q)X=B?.();else if(P)X=V(P);else if(U&&(Z?Gz($[0])&&$[0].isPending():$.some((Q)=>Gz(Q)&&Q.isPending())))X=U();else X=D(K)}catch(Q){X=V([Q instanceof Error?Q:Error(String(Q))])}if(typeof X==="function")return X;if(X instanceof Promise){let Q=T,j=new AbortController;jz(Q,()=>j.abort()),X.then((H)=>{if(!j.signal.aborted&&typeof H==="function")jz(Q,H)}).catch((H)=>{V([H instanceof Error?H:Error(String(H))])})}}function VJ(z,J){if(h(e,z,Bz),J?.value!==void 0)O(e,J.value,J?.guard);let Z={value:J?.value,sinks:null,sinksTail:null,equals:J?.equals??p,guard:J?.guard,stop:void 0};return{[Symbol.toStringTag]:e,get(){if(G){if(!Z.sinks)Z.stop=z(($)=>{O(e,$,Z.guard),g(Z,$)});_(Z,G)}return qz(e,Z.value),Z.value}}}function NJ(z){return f(z,e)}function DJ(z,J){let Z={},$={},B={},U=!1,D=Object.keys(z),V=Object.keys(J);for(let P of V)if(P in z){if(!s(z[P],J[P]))$[P]=J[P],U=!0}else Z[P]=J[P],U=!0;for(let P of D)if(!(P in J))B[P]=void 0,U=!0;return{add:Z,change:$,remove:B,changed:U}}function Oz(z,J){O(zz,z,k);let Z=new Map,$=(X,Q)=>{if(O(`${zz} for key "${X}"`,Q),Array.isArray(Q))Z.set(X,Vz(Q));else if(k(Q))Z.set(X,Oz(Q));else Z.set(X,t(Q))},B=(X)=>{if(Array.isArray(X))return"list";if(k(X))return"store";return"state"},U=(X)=>{if(Kz(X))return"list";if(Lz(X))return"store";return"state"},D=()=>{let X={};for(let[Q,j]of Z)X[Q]=j.get();return X},V={fn:D,value:z,flags:N,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:s,error:void 0},P=(X)=>{let Q=!1;for(let H in X.add)$(H,X.add[H]),Q=!0;let j=!1;for(let H in X.change){j=!0;break}if(j)u(()=>{for(let H in X.change){let W=X.change[H];O(`${zz} for key "${H}"`,W);let M=Z.get(H);if(M)if(B(W)!==U(M))$(H,W),Q=!0;else M.set(W)}});for(let H in X.remove)Z.delete(H),Q=!0;if(Q)V.flags|=b;return X.changed},q=v(V,J?.watched);for(let X of Object.keys(z))$(X,z[X]);let K={[Symbol.toStringTag]:zz,[Symbol.isConcatSpreadable]:!1,*[Symbol.iterator](){q();for(let[X,Q]of Z)yield[X,Q]},keys(){return q(),Z.keys()},byKey(X){return Z.get(X)},get(){if(q(),V.sources){if(V.flags){let X=V.flags&b;if(V.value=Y(D),X){if(V.flags=N,A(V),V.error)throw V.error}else V.flags=S}}else if(A(V),V.error)throw V.error;return V.value},set(X){let Q=V.flags&N?Y(D):V.value,j=DJ(Q,X);if(P(j)){V.flags|=N;for(let H=V.sinks;H;H=H.nextSink)w(H.sink);if(x===0)F()}},update(X){K.set(X(Y(()=>K.get())))},add(X,Q){if(Z.has(X))throw new d(zz,X,Q);$(X,Q),V.flags|=N|b;for(let j=V.sinks;j;j=j.nextSink)w(j.sink);if(x===0)F();return X},remove(X){if(Z.delete(X)){V.flags|=N|b;for(let j=V.sinks;j;j=j.nextSink)w(j.sink);if(x===0)F()}}};return new Proxy(K,{get(X,Q){if(Q in X)return Reflect.get(X,Q);if(typeof Q!=="symbol")return X.byKey(Q)},set(X,Q){throw new Qz(String(Q),"assign to")},deleteProperty(X,Q){throw new Qz(String(Q),"delete")},defineProperty(X,Q){throw new Qz(String(Q),"define")},has(X,Q){if(Q in X)return!0;return X.byKey(String(Q))!==void 0},ownKeys(X){return Array.from(X.keys())},getOwnPropertyDescriptor(X,Q){if(Q in X)return Reflect.getOwnPropertyDescriptor(X,Q);if(typeof Q==="symbol")return;let j=X.byKey(String(Q));return j?{enumerable:!0,configurable:!0,writable:!0,value:j}:void 0}})}function Lz(z){return f(z,zz)}var GJ=new Set([n,l,a,e,Jz,L,i,zz]);function PJ(z,J){return Zz(z)?Dz(z,J):Nz(z,J)}function RJ(z){if(wz(z))return z;if(z==null)throw new Hz("createSignal",z);if(Zz(z))return Dz(z);if(c(z))return Nz(z);if(Array.isArray(z)&&z.every((J)=>J!=null))return Vz(z);if(k(z))return Oz(z);return t(z)}function KJ(z){if(oz(z))return z;if(z==null||c(z)||wz(z))throw new Hz("createMutableSignal",z);if(Array.isArray(z)&&z.every((J)=>J!=null))return Vz(z);if(k(z))return Oz(z);return t(z)}function OJ(z){return kz(z)||Gz(z)}function wz(z){return z!=null&&GJ.has(z[Symbol.toStringTag])}function oz(z){return Rz(z)||Lz(z)||Kz(z)}var gz=new WeakSet;function rz(z){if(wz(z))return!0;return z!==null&&typeof z==="object"&&"get"in z&&typeof z.get==="function"}function wJ(z,J){O(Jz,z,rz);let Z=z,$=J?.guard,B={fn:()=>Z.get(),value:void 0,flags:N,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:J?.equals??p,error:void 0},U=()=>{if(G)_(B,G);if(A(B),B.error)throw B.error;return B.value},D=(P)=>{if(gz.has(B))throw Error("[Slot] Circular delegation detected in set()");gz.add(B);try{if(nz(Z))return void Z.set(P);if("set"in Z&&typeof Z.set==="function")O(Jz,P,$),Z.set(P);else throw new Yz(Jz)}finally{gz.delete(B)}},V=(P)=>{O(Jz,P,rz),Z=P,B.flags|=N;for(let q=B.sinks;q;q=q.nextSink)w(q.sink);if(x===0)F()};return{[Symbol.toStringTag]:Jz,configurable:!0,enumerable:!0,get:U,set:D,replace:V,current:()=>Z}}function nz(z){return f(z,Jz)}export{xz as valueString,Y as untrack,WJ as unown,UJ as match,Gz as isTask,Lz as isStore,Rz as isState,nz as isSlot,f as isSignalOfType,wz as isSignal,NJ as isSensor,k as isRecord,az as isObjectOfType,oz as isMutableSignal,kz as isMemo,Kz as isList,c as isFunction,zJ as isEqual,OJ as isComputed,qJ as isCollection,Zz as isAsyncFunction,Dz as createTask,Oz as createStore,t as createState,wJ as createSlot,RJ as createSignal,VJ as createSensor,XJ as createScope,KJ as createMutableSignal,Nz as createMemo,Vz as createList,MJ as createEffect,PJ as createComputed,QJ as createCollection,u as batch,$z as UnsetSignalValueError,Sz as SKIP_EQUALITY,Iz as RequiredOwnerError,Yz as ReadonlySignalError,mz as PromiseValueError,Pz as NullishSignalValueError,Qz as InvalidStoreMutationError,Hz as InvalidSignalValueError,Tz as InvalidCallbackError,Fz as EffectConvergenceError,d as DuplicateKeyError,p as DEFAULT_EQUALITY,s as DEEP_EQUALITY,Cz as CircularDependencyError}; +// src/util.ts +var ASYNC_FUNCTION_PROTO = Object.getPrototypeOf(async () => {}); +function isFunction(fn) { + return typeof fn === "function"; +} +function isAsyncFunction(fn) { + return isFunction(fn) && Object.getPrototypeOf(fn) === ASYNC_FUNCTION_PROTO; +} +function isSyncFunction(fn) { + return isFunction(fn) && Object.getPrototypeOf(fn) !== ASYNC_FUNCTION_PROTO; +} +function isObjectOfType(value, type) { + return Object.prototype.toString.call(value) === `[object ${type}]`; +} +function isSignalOfType(value, type) { + return value != null && value[Symbol.toStringTag] === type; +} +function isRecord(value) { + return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype; +} +function valueString(value) { + if (typeof value === "string") + return `"${value}"`; + if (value != null && typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + return String(value); +} + +// src/errors.ts +class CircularDependencyError extends Error { + constructor(where) { + super(`[${where}] Circular dependency detected`); + this.name = "CircularDependencyError"; + } +} + +class EffectConvergenceError extends Error { + constructor(passes) { + super(`[Effect] Effects did not settle after ${passes} flush passes — check for effects that write to signals they depend on`); + this.name = "EffectConvergenceError"; + } +} + +class NullishSignalValueError extends TypeError { + constructor(where) { + super(`[${where}] Signal value cannot be null or undefined`); + this.name = "NullishSignalValueError"; + } +} + +class UnsetSignalValueError extends Error { + constructor(where) { + super(`[${where}] Signal value is unset`); + this.name = "UnsetSignalValueError"; + } +} + +class InvalidSignalValueError extends TypeError { + constructor(where, value) { + super(`[${where}] Signal value ${valueString(value)} is invalid`); + this.name = "InvalidSignalValueError"; + } +} + +class InvalidCallbackError extends TypeError { + constructor(where, value) { + super(`[${where}] Callback ${valueString(value)} is invalid`); + this.name = "InvalidCallbackError"; + } +} + +class ReadonlySignalError extends Error { + constructor(where) { + super(`[${where}] Signal is read-only`); + this.name = "ReadonlySignalError"; + } +} + +class RequiredOwnerError extends Error { + constructor(where) { + super(`[${where}] Active owner is required`); + this.name = "RequiredOwnerError"; + } +} + +class PromiseValueError extends TypeError { + constructor(where) { + super(`[${where}] Callback returned a Promise — use an async callback to create a Task instead`); + this.name = "PromiseValueError"; + } +} + +class DuplicateKeyError extends Error { + constructor(where, key, value) { + super(`[${where}] Could not add key "${key}"${value != null ? ` with value ${JSON.stringify(value)}` : ""} because it already exists`); + this.name = "DuplicateKeyError"; + } +} + +class InvalidStoreMutationError extends TypeError { + constructor(prop, action) { + const guidance = action === "delete" ? `use store.remove(${JSON.stringify(prop)})` : `use store.${prop}.set(value), store.set(next), or store.add(key, value)`; + super(`[Store] Cannot ${action} property "${prop}" directly — ${guidance}`); + this.name = "InvalidStoreMutationError"; + } +} +function validateSignalValue(where, value, guard) { + if (value == null) + throw new NullishSignalValueError(where); + if (guard && !guard(value)) + throw new InvalidSignalValueError(where, value); +} +function validateReadValue(where, value) { + if (value == null) + throw new UnsetSignalValueError(where); +} +function validateCallback(where, value, guard = isFunction) { + if (!guard(value)) + throw new InvalidCallbackError(where, value); +} +// src/graph.ts +var TYPE_STATE = "State"; +var TYPE_MEMO = "Memo"; +var TYPE_TASK = "Task"; +var TYPE_SENSOR = "Sensor"; +var TYPE_LIST = "List"; +var TYPE_COLLECTION = "Collection"; +var TYPE_STORE = "Store"; +var TYPE_SLOT = "Slot"; +var FLAG_CLEAN = 0; +var FLAG_CHECK = 1 << 0; +var FLAG_DIRTY = 1 << 1; +var FLAG_RUNNING = 1 << 2; +var FLAG_RELINK = 1 << 3; +var activeSink = null; +var activeOwner = null; +var queuedEffects = []; +var batchDepth = 0; +var flushing = false; +var DEFAULT_EQUALITY = (a, b) => a === b; +var SKIP_EQUALITY = (_a, _b) => false; +var deepEqual = (a, b) => deepEqualInner(a, b, new WeakSet); +var deepEqualInner = (a, b, seen) => { + if (Object.is(a, b)) + return true; + if (typeof a !== typeof b) + return false; + if (a == null || typeof a !== "object" || b == null || typeof b !== "object") + return false; + if (seen.has(a)) + return true; + seen.add(a); + try { + const aIsArray = Array.isArray(a); + if (aIsArray !== Array.isArray(b)) + return false; + if (aIsArray) { + const aa = a; + const ba = b; + if (aa.length !== ba.length) + return false; + for (let i = 0;i < aa.length; i++) + if (!deepEqualInner(aa[i], ba[i], seen)) + return false; + return true; + } + if (a instanceof Date && b instanceof Date) + return a.getTime() === b.getTime(); + if (a instanceof RegExp && b instanceof RegExp) + return a.source === b.source && a.flags === b.flags; + if (isRecord(a) && isRecord(b)) { + const aKeys = Object.keys(a); + if (aKeys.length !== Object.keys(b).length) + return false; + for (const key of aKeys) { + if (!(key in b)) + return false; + if (!deepEqualInner(a[key], b[key], seen)) + return false; + } + return true; + } + return false; + } finally { + seen.delete(a); + } +}; +var DEEP_EQUALITY = (a, b) => deepEqual(a, b); +var isEqual = DEEP_EQUALITY; +function isValidEdge(checkEdge, node) { + const sourcesTail = node.sourcesTail; + if (sourcesTail) { + let edge = node.sources; + while (edge) { + if (edge === checkEdge) + return true; + if (edge === sourcesTail) + break; + edge = edge.nextSource; + } + } + return false; +} +function link(source, sink) { + const prevSource = sink.sourcesTail; + if (prevSource?.source === source) + return; + let nextSource = null; + const isRecomputing = sink.flags & FLAG_RUNNING; + if (isRecomputing) { + nextSource = prevSource ? prevSource.nextSource : sink.sources; + if (nextSource?.source === source) { + sink.sourcesTail = nextSource; + return; + } + } + const prevSink = source.sinksTail; + if (prevSink?.sink === sink && (!isRecomputing || isValidEdge(prevSink, sink))) + return; + const newEdge = { source, sink, nextSource, prevSink, nextSink: null }; + sink.sourcesTail = source.sinksTail = newEdge; + if (prevSource) + prevSource.nextSource = newEdge; + else + sink.sources = newEdge; + if (prevSink) + prevSink.nextSink = newEdge; + else + source.sinks = newEdge; +} +function unlink(edge) { + const { source, nextSource, nextSink, prevSink } = edge; + if (nextSink) + nextSink.prevSink = prevSink; + else + source.sinksTail = prevSink; + if (prevSink) + prevSink.nextSink = nextSink; + else + source.sinks = nextSink; + if (!source.sinks) { + if (source.stop) { + source.stop(); + source.stop = undefined; + } + if ("sources" in source && source.sources) { + const sinkNode = source; + sinkNode.sourcesTail = null; + trimSources(sinkNode); + sinkNode.flags |= FLAG_DIRTY; + } + } + return nextSource; +} +function trimSources(node) { + const tail = node.sourcesTail; + let source = tail ? tail.nextSource : node.sources; + while (source) + source = unlink(source); + if (tail) + tail.nextSource = null; + else + node.sources = null; +} +function propagate(node, newFlag = FLAG_DIRTY) { + const flags = node.flags; + if ("sinks" in node) { + if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) + return; + node.flags = flags | newFlag; + if ("controller" in node && node.controller) { + node.controller.abort(); + node.controller = undefined; + } + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink, FLAG_CHECK); + } else { + if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) + return; + const wasQueued = flags & (FLAG_DIRTY | FLAG_CHECK); + node.flags = flags & FLAG_RUNNING | newFlag; + if (!wasQueued) + queuedEffects.push(node); + } +} +function setState(node, next) { + if (node.equals(node.value, next)) + return; + node.value = next; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); +} +function registerCleanup(owner, fn) { + if (!owner.cleanup) + owner.cleanup = fn; + else if (Array.isArray(owner.cleanup)) + owner.cleanup.push(fn); + else + owner.cleanup = [owner.cleanup, fn]; +} +function runCleanup(owner) { + if (!owner.cleanup) + return; + if (Array.isArray(owner.cleanup)) + for (let i = 0;i < owner.cleanup.length; i++) + owner.cleanup[i](); + else + owner.cleanup(); + owner.cleanup = null; +} +function recomputeMemo(node) { + const prevWatcher = activeSink; + activeSink = node; + node.sourcesTail = null; + node.flags = FLAG_RUNNING; + let changed = false; + try { + const next = node.fn(node.value); + if (next instanceof Promise) + throw new PromiseValueError(TYPE_MEMO); + if (node.error || !node.equals(next, node.value)) { + node.value = next; + node.error = undefined; + changed = true; + } + } catch (err) { + changed = true; + node.error = err instanceof Error ? err : new Error(String(err)); + } finally { + activeSink = prevWatcher; + trimSources(node); + } + if (changed) { + for (let e = node.sinks;e; e = e.nextSink) + if (e.sink.flags & FLAG_CHECK) + e.sink.flags |= FLAG_DIRTY; + } + node.flags = FLAG_CLEAN; +} +function recomputeTask(node) { + node.controller?.abort(); + const controller = new AbortController; + node.controller = controller; + node.error = undefined; + const prevWatcher = activeSink; + activeSink = node; + node.sourcesTail = null; + node.flags = FLAG_RUNNING; + let promise; + try { + promise = node.fn(node.value, controller.signal); + } catch (err) { + node.controller = undefined; + node.error = err instanceof Error ? err : new Error(String(err)); + node.flags = FLAG_CLEAN; + setState(node.pendingNode, false); + return; + } finally { + activeSink = prevWatcher; + trimSources(node); + } + setState(node.pendingNode, true); + promise.then((next) => { + if (controller.signal.aborted) + return; + node.controller = undefined; + batch(() => { + if (node.error || !node.equals(next, node.value)) { + node.value = next; + node.error = undefined; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + } + setState(node.pendingNode, false); + }); + }, (err) => { + if (controller.signal.aborted) + return; + node.controller = undefined; + const error = err instanceof Error ? err : new Error(String(err)); + batch(() => { + if (!node.error || error.name !== node.error.name || error.message !== node.error.message) { + node.error = error; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + } + setState(node.pendingNode, false); + }); + }); + node.flags = FLAG_CLEAN; +} +function runEffect(node) { + runCleanup(node); + const prevContext = activeSink; + const prevOwner = activeOwner; + activeSink = activeOwner = node; + node.sourcesTail = null; + node.flags = FLAG_RUNNING; + try { + const out = node.fn(); + if (typeof out === "function") + registerCleanup(node, out); + } finally { + activeSink = prevContext; + activeOwner = prevOwner; + trimSources(node); + node.flags &= FLAG_DIRTY | FLAG_CHECK; + } +} +function refresh(node) { + if (node.flags & FLAG_CHECK) { + for (let e = node.sources;e; e = e.nextSource) { + if ("fn" in e.source) + refresh(e.source); + if (node.flags & FLAG_DIRTY) + break; + } + } + if (node.flags & FLAG_RUNNING) { + throw new CircularDependencyError("controller" in node ? TYPE_TASK : ("value" in node) ? TYPE_MEMO : "Effect"); + } + if (node.flags & FLAG_DIRTY) { + if ("controller" in node) + recomputeTask(node); + else if ("value" in node) + recomputeMemo(node); + else + runEffect(node); + } else { + node.flags = FLAG_CLEAN; + } +} +var MAX_FLUSH_PASSES = 1000; +function flush() { + if (flushing) + return; + flushing = true; + let errors; + let passes = 0; + try { + while (queuedEffects.length > 0) { + if (++passes > MAX_FLUSH_PASSES) { + queuedEffects.length = 0; + if (!errors) + errors = []; + errors.push(new EffectConvergenceError(MAX_FLUSH_PASSES)); + break; + } + const batch = queuedEffects.slice(); + queuedEffects.length = 0; + for (let i = 0;i < batch.length; i++) { + const effect = batch[i]; + if (effect.flags & FLAG_RUNNING) + continue; + if (effect.flags & (FLAG_DIRTY | FLAG_CHECK)) { + try { + refresh(effect); + } catch (err) { + if (!errors) + errors = []; + errors.push(err); + } + } + } + } + } finally { + flushing = false; + } + if (errors) { + if (errors.length === 1) + throw errors[0]; + throw new AggregateError(errors, "Multiple effects threw during flush"); + } +} +function scheduleEffect(node) { + if (node.flags & (FLAG_DIRTY | FLAG_CHECK)) { + queuedEffects.push(node); + if (batchDepth === 0) + flush(); + } +} +function batch(fn) { + batchDepth++; + try { + fn(); + } finally { + batchDepth--; + if (batchDepth === 0) + flush(); + } +} +function untrack(fn) { + const prev = activeSink; + activeSink = null; + try { + return fn(); + } finally { + activeSink = prev; + } +} +function createScope(fn, options) { + const prevOwner = activeOwner; + const scope = { cleanup: null }; + activeOwner = scope; + const dispose = () => runCleanup(scope); + try { + const out = fn(); + if (typeof out === "function") + registerCleanup(scope, out); + return dispose; + } finally { + activeOwner = prevOwner; + if (!options?.root && prevOwner) + registerCleanup(prevOwner, dispose); + } +} +function unown(fn) { + const prev = activeOwner; + activeOwner = null; + try { + return fn(); + } finally { + activeOwner = prev; + } +} +function makeSubscribe(node, onWatch) { + return onWatch ? () => { + if (activeSink) { + if (!node.sinks) + node.stop = onWatch(); + link(node, activeSink); + } + } : () => { + if (activeSink) + link(node, activeSink); + }; +} +// src/nodes/state.ts +function createState(value, options) { + validateSignalValue(TYPE_STATE, value, options?.guard); + const node = { + value, + sinks: null, + sinksTail: null, + equals: options?.equals ?? DEFAULT_EQUALITY, + guard: options?.guard + }; + return { + [Symbol.toStringTag]: TYPE_STATE, + get() { + if (activeSink) + link(node, activeSink); + return node.value; + }, + set(next) { + validateSignalValue(TYPE_STATE, next, node.guard); + setState(node, next); + }, + update(fn) { + validateCallback(TYPE_STATE, fn); + const next = fn(node.value); + validateSignalValue(TYPE_STATE, next, node.guard); + setState(node, next); + } + }; +} +function isState(value) { + return isSignalOfType(value, TYPE_STATE); +} + +// src/nodes/list.ts +function keysEqual(a, b) { + if (a.length !== b.length) + return false; + for (let i = 0;i < a.length; i++) + if (a[i] !== b[i]) + return false; + return true; +} +function getKeyGenerator(keyConfig) { + let keyCounter = 0; + const contentBased = typeof keyConfig === "function"; + return [ + typeof keyConfig === "string" ? () => `${keyConfig}${keyCounter++}` : contentBased ? (item) => keyConfig(item) || String(keyCounter++) : () => String(keyCounter++), + contentBased + ]; +} +function diffPositional(prev, next, prevKeys, generateKey, itemEquals) { + const add = {}; + const change = {}; + const remove = {}; + const nextKeys = []; + let changed = false; + const minLen = Math.min(prev.length, next.length); + for (let i = 0;i < minLen; i++) { + const key = prevKeys[i]; + nextKeys.push(key); + if (!itemEquals(prev[i], next[i])) { + change[key] = next[i]; + changed = true; + } + } + for (let i = minLen;i < next.length; i++) { + const val = next[i]; + const key = generateKey(val); + nextKeys.push(key); + add[key] = val; + changed = true; + } + for (let i = minLen;i < prev.length; i++) { + remove[prevKeys[i]] = null; + changed = true; + } + return { add, change, remove, newKeys: nextKeys, changed }; +} +function diffArrays(prev, next, prevKeys, generateKey, contentBased, itemEquals) { + if (!contentBased) + return diffPositional(prev, next, prevKeys, generateKey, itemEquals); + const add = {}; + const change = {}; + const remove = {}; + const nextKeys = []; + let changed = false; + const prevByKey = new Map; + for (let i = 0;i < prev.length; i++) { + const key = prevKeys[i]; + const item = prev[i]; + if (key && item !== undefined) + prevByKey.set(key, item); + } + const seenKeys = new Set; + for (let i = 0;i < next.length; i++) { + const val = next[i]; + validateSignalValue(`${TYPE_LIST} item at index ${i}`, val); + const key = generateKey(val); + if (seenKeys.has(key)) + throw new DuplicateKeyError(TYPE_LIST, key, val); + nextKeys.push(key); + seenKeys.add(key); + if (!prevByKey.has(key)) { + add[key] = val; + changed = true; + } else if (!itemEquals(prevByKey.get(key), val)) { + change[key] = val; + changed = true; + } + } + for (const [key] of prevByKey) { + if (!seenKeys.has(key)) { + remove[key] = null; + changed = true; + } + } + if (!changed && !keysEqual(prevKeys, nextKeys)) + changed = true; + return { add, change, remove, newKeys: nextKeys, changed }; +} +function createList(value, options) { + validateSignalValue(TYPE_LIST, value, Array.isArray); + const signals = new Map; + let keys = []; + const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig); + const itemEquals = options?.itemEquals ?? DEEP_EQUALITY; + const itemFactory = options?.createItem ?? ((item) => createState(item, { equals: itemEquals })); + const buildValue = () => { + const result = []; + for (const key of keys) { + const v = signals.get(key)?.get(); + if (v !== undefined) + result.push(v); + } + return result; + }; + const node = { + fn: buildValue, + value, + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + equals: DEEP_EQUALITY, + error: undefined + }; + const applyChanges = (changes) => { + let structural = false; + for (const key in changes.add) { + const val = changes.add[key]; + validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val); + signals.set(key, itemFactory(val)); + structural = true; + } + let hasChange = false; + for (const _key in changes.change) { + hasChange = true; + break; + } + if (hasChange) { + batch(() => { + for (const key in changes.change) { + const val = changes.change[key]; + validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val); + const signal = signals.get(key); + if (signal) + signal.set(val); + } + }); + } + for (const key in changes.remove) { + signals.delete(key); + const index = keys.indexOf(key); + if (index !== -1) + keys.splice(index, 1); + structural = true; + } + if (structural) + node.flags |= FLAG_RELINK; + return changes.changed; + }; + const subscribe = makeSubscribe(node, options?.watched); + for (let i = 0;i < value.length; i++) { + const val = value[i]; + if (val == null) + throw new NullishSignalValueError(`${TYPE_LIST} item ${i}`); + let key = keys[i]; + if (!key) { + key = generateKey(val); + keys[i] = key; + } + signals.set(key, itemFactory(val)); + } + node.value = value; + node.flags = 0; + const list = { + [Symbol.toStringTag]: TYPE_LIST, + [Symbol.isConcatSpreadable]: true, + *[Symbol.iterator]() { + subscribe(); + for (const key of keys) { + const signal = signals.get(key); + if (signal) + yield signal; + } + }, + get length() { + subscribe(); + return keys.length; + }, + get() { + subscribe(); + if (node.sources) { + if (node.flags) { + const relink = node.flags & FLAG_RELINK; + node.value = untrack(buildValue); + if (relink) { + node.flags = FLAG_DIRTY; + refresh(node); + if (node.error) + throw node.error; + } else { + node.flags = FLAG_CLEAN; + } + } + } else { + refresh(node); + if (node.error) + throw node.error; + } + return node.value; + }, + set(next) { + const prev = node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value; + const changes = diffArrays(prev, next, keys, generateKey, contentBased, itemEquals); + if (changes.changed) { + keys = changes.newKeys; + applyChanges(changes); + node.flags |= FLAG_DIRTY; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + } + }, + update(fn) { + list.set(fn(untrack(() => list.get()))); + }, + at(index) { + subscribe(); + const key = keys[index]; + return key !== undefined ? signals.get(key) : undefined; + }, + keys() { + subscribe(); + return keys.values(); + }, + byKey(key) { + subscribe(); + return signals.get(key); + }, + keyAt(index) { + subscribe(); + return keys[index]; + }, + indexOfKey(key) { + subscribe(); + return keys.indexOf(key); + }, + add(value2) { + const key = generateKey(value2); + if (signals.has(key)) + throw new DuplicateKeyError(TYPE_LIST, key, value2); + keys.push(key); + validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value2); + signals.set(key, itemFactory(value2)); + node.flags |= FLAG_DIRTY | FLAG_RELINK; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + return key; + }, + remove(keyOrIndex) { + const key = typeof keyOrIndex === "number" ? keys[keyOrIndex] : keyOrIndex; + if (key === undefined) + return; + const ok = signals.delete(key); + if (ok) { + const index = typeof keyOrIndex === "number" ? keyOrIndex : keys.indexOf(key); + if (index >= 0) + keys.splice(index, 1); + node.flags |= FLAG_DIRTY | FLAG_RELINK; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + } + }, + replace(key, value2) { + const signal = signals.get(key); + if (!signal) + return; + validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value2); + if (itemEquals(untrack(() => signal.get()), value2)) + return; + batch(() => { + signal.set(value2); + node.flags |= FLAG_DIRTY; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + }); + if (batchDepth === 0) + flush(); + }, + sort(compareFn) { + const entries = []; + untrack(() => { + for (const key of keys) { + const v = signals.get(key)?.get(); + if (v !== undefined) + entries.push([key, v]); + } + }); + entries.sort(isFunction(compareFn) ? (a, b) => compareFn(a[1], b[1]) : (a, b) => String(a[1]).localeCompare(String(b[1]))); + const newOrder = []; + for (const [key] of entries) + newOrder.push(key); + if (!keysEqual(keys, newOrder)) { + keys = newOrder; + node.flags |= FLAG_DIRTY; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + } + }, + splice(start, deleteCount, ...items) { + const length = keys.length; + const actualStart = start < 0 ? Math.max(0, length + start) : Math.min(start, length); + const actualDeleteCount = Math.max(0, Math.min(deleteCount ?? Math.max(0, length - Math.max(0, actualStart)), length - actualStart)); + const add = {}; + const remove = {}; + let hasRemove = false; + untrack(() => { + for (let i = 0;i < actualDeleteCount; i++) { + const index = actualStart + i; + const key = keys[index]; + if (key) { + const signal = signals.get(key); + if (signal) { + remove[key] = signal.get(); + hasRemove = true; + } + } + } + }); + const newOrder = keys.slice(0, actualStart); + const change = {}; + let hasAdd = false; + let hasChange = false; + for (const item of items) { + const key = generateKey(item); + if (key in remove) { + delete remove[key]; + change[key] = item; + hasChange = true; + } else if (signals.has(key)) { + throw new DuplicateKeyError(TYPE_LIST, key, item); + } else { + add[key] = item; + hasAdd = true; + } + newOrder.push(key); + } + newOrder.push(...keys.slice(actualStart + actualDeleteCount)); + const changed = hasAdd || hasRemove || hasChange; + if (changed) { + applyChanges({ + add, + change, + remove, + changed + }); + keys = newOrder; + node.flags |= FLAG_DIRTY; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + } + return Object.values(remove); + }, + deriveCollection(cb) { + return deriveCollection(list, cb); + } + }; + return list; +} +function isList(value) { + return isSignalOfType(value, TYPE_LIST); +} + +// src/nodes/memo.ts +function createMemo(fn, options) { + validateCallback(TYPE_MEMO, fn, isSyncFunction); + if (options?.value !== undefined) + validateSignalValue(TYPE_MEMO, options.value, options?.guard); + const node = { + fn, + value: options?.value, + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + equals: options?.equals ?? DEFAULT_EQUALITY, + error: undefined, + stop: undefined + }; + const watched = options?.watched; + const subscribe = makeSubscribe(node, watched ? () => watched(() => { + propagate(node); + if (batchDepth === 0) + flush(); + }) : undefined); + return { + [Symbol.toStringTag]: TYPE_MEMO, + get() { + subscribe(); + refresh(node); + if (node.error) + throw node.error; + validateReadValue(TYPE_MEMO, node.value); + return node.value; + } + }; +} +function isMemo(value) { + return isSignalOfType(value, TYPE_MEMO); +} + +// src/nodes/task.ts +function createTask(fn, options) { + validateCallback(TYPE_TASK, fn, isAsyncFunction); + if (options?.value !== undefined) + validateSignalValue(TYPE_TASK, options.value, options?.guard); + const pendingNode = { + value: false, + sinks: null, + sinksTail: null, + equals: DEFAULT_EQUALITY + }; + const node = { + fn, + value: options?.value, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + flags: FLAG_DIRTY, + equals: options?.equals ?? DEFAULT_EQUALITY, + controller: undefined, + error: undefined, + stop: undefined, + pendingNode + }; + const watched = options?.watched; + const subscribe = makeSubscribe(node, watched ? () => watched(() => { + propagate(node); + if (batchDepth === 0) + flush(); + }) : undefined); + const pendingSubscribe = makeSubscribe(pendingNode); + return { + [Symbol.toStringTag]: TYPE_TASK, + get() { + subscribe(); + refresh(node); + if (node.error) + throw node.error; + validateReadValue(TYPE_TASK, node.value); + return node.value; + }, + isPending() { + pendingSubscribe(); + return node.pendingNode.value; + }, + abort() { + node.controller?.abort(); + node.controller = undefined; + setState(node.pendingNode, false); + } + }; +} +function isTask(value) { + return isSignalOfType(value, TYPE_TASK); +} + +// src/nodes/collection.ts +function deriveCollection(source, callback) { + validateCallback(TYPE_COLLECTION, callback); + const isAsync = isAsyncFunction(callback); + const signals = new Map; + let keys = []; + const addSignal = (key) => { + const signal = isAsync ? createTask(async (prev, abort) => { + const itemSignal = untrack(() => source.byKey(key)); + if (!itemSignal) + return prev; + const sourceValue = itemSignal.get(); + if (sourceValue == null) + return prev; + return callback(sourceValue, abort); + }) : createMemo(() => { + const itemSignal = untrack(() => source.byKey(key)); + if (!itemSignal) + return; + const sourceValue = itemSignal.get(); + if (sourceValue == null) + return; + return callback(sourceValue); + }); + signals.set(key, signal); + }; + function syncKeys(nextKeys) { + if (!keysEqual(keys, nextKeys)) { + const nextSet = new Set(nextKeys); + for (const key of keys) + if (!nextSet.has(key)) + signals.delete(key); + for (const key of nextKeys) + if (!signals.has(key)) + addSignal(key); + keys = nextKeys; + node.flags |= FLAG_RELINK; + } + } + function buildValue() { + syncKeys(Array.from(source.keys())); + const result = []; + for (const key of keys) { + try { + const v = signals.get(key)?.get(); + if (v != null) + result.push(v); + } catch (e) { + if (!(e instanceof UnsetSignalValueError)) + throw e; + } + } + return result; + } + const valuesEqual = (a, b) => { + if (a.length !== b.length) + return false; + for (let i = 0;i < a.length; i++) + if (a[i] !== b[i]) + return false; + return true; + }; + const node = { + fn: buildValue, + value: [], + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + equals: valuesEqual, + error: undefined + }; + function ensureFresh() { + if (node.sources) { + if (node.flags) { + node.value = untrack(buildValue); + if (node.flags & FLAG_RELINK) { + node.flags = FLAG_DIRTY; + refresh(node); + if (node.error) + throw node.error; + } else { + node.flags = FLAG_CLEAN; + } + } + } else if (node.sinks) { + refresh(node); + if (node.error) + throw node.error; + } else { + node.value = untrack(buildValue); + } + } + const initialKeys = Array.from(untrack(() => source.keys())); + for (const key of initialKeys) + addSignal(key); + keys = initialKeys; + const collection = { + [Symbol.toStringTag]: TYPE_COLLECTION, + [Symbol.isConcatSpreadable]: true, + *[Symbol.iterator]() { + if (activeSink) + link(node, activeSink); + ensureFresh(); + for (const key of keys) { + const signal = signals.get(key); + if (signal) + yield signal; + } + }, + get length() { + if (activeSink) + link(node, activeSink); + ensureFresh(); + return keys.length; + }, + keys() { + if (activeSink) + link(node, activeSink); + ensureFresh(); + return keys.values(); + }, + get() { + if (activeSink) + link(node, activeSink); + ensureFresh(); + return node.value; + }, + at(index) { + if (activeSink) + link(node, activeSink); + ensureFresh(); + const key = keys[index]; + return key !== undefined ? signals.get(key) : undefined; + }, + byKey(key) { + if (activeSink) + link(node, activeSink); + ensureFresh(); + return signals.get(key); + }, + keyAt(index) { + if (activeSink) + link(node, activeSink); + ensureFresh(); + return keys[index]; + }, + indexOfKey(key) { + if (activeSink) + link(node, activeSink); + ensureFresh(); + return keys.indexOf(key); + }, + deriveCollection(cb) { + return deriveCollection(collection, cb); + } + }; + return collection; +} +function createCollection(watched, options) { + const value = options?.value ?? []; + if (value.length) + validateSignalValue(TYPE_COLLECTION, value, Array.isArray); + validateCallback(TYPE_COLLECTION, watched, isSyncFunction); + const signals = new Map; + const keys = []; + const itemToKey = new Map; + const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig); + const resolveKey = (item) => itemToKey.get(item) ?? (contentBased ? generateKey(item) : undefined); + const itemFactory = options?.createItem ?? ((item) => createState(item, { + equals: options?.itemEquals ?? DEEP_EQUALITY + })); + function buildValue() { + const result = []; + for (const key of keys) { + try { + const v = signals.get(key)?.get(); + if (v != null) + result.push(v); + } catch (e) { + if (!(e instanceof UnsetSignalValueError)) + throw e; + } + } + return result; + } + const node = { + fn: buildValue, + value, + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + equals: SKIP_EQUALITY, + error: undefined + }; + for (const item of value) { + const key = generateKey(item); + signals.set(key, itemFactory(item)); + itemToKey.set(item, key); + keys.push(key); + } + node.value = value; + node.flags = FLAG_DIRTY; + const onChanges = (changes) => { + const { add, change, remove } = changes; + if (!add?.length && !change?.length && !remove?.length) + return; + let structural = false; + batch(() => { + if (add) { + const staged = new Map; + for (const item of add) { + const key = generateKey(item); + if (signals.has(key) || staged.has(key)) + throw new DuplicateKeyError(TYPE_COLLECTION, key, item); + staged.set(key, item); + } + for (const [key, item] of staged) { + signals.set(key, itemFactory(item)); + itemToKey.set(item, key); + if (!keys.includes(key)) + keys.push(key); + structural = true; + } + } + if (change) { + for (const item of change) { + const key = resolveKey(item); + if (!key) + continue; + const signal = signals.get(key); + if (signal && isState(signal)) { + itemToKey.delete(untrack(() => signal.get())); + signal.set(item); + itemToKey.set(item, key); + } + } + } + if (remove) { + for (const item of remove) { + const key = resolveKey(item); + if (!key) + continue; + itemToKey.delete(item); + signals.delete(key); + const index = keys.indexOf(key); + if (index !== -1) + keys.splice(index, 1); + structural = true; + } + } + node.flags = FLAG_DIRTY | (structural ? FLAG_RELINK : 0); + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + }); + }; + const subscribe = makeSubscribe(node, () => watched(onChanges)); + const collection = { + [Symbol.toStringTag]: TYPE_COLLECTION, + [Symbol.isConcatSpreadable]: true, + *[Symbol.iterator]() { + subscribe(); + for (const key of keys) { + const signal = signals.get(key); + if (signal) + yield signal; + } + }, + get length() { + subscribe(); + return keys.length; + }, + keys() { + subscribe(); + return keys.values(); + }, + get() { + subscribe(); + if (node.sources) { + if (node.flags) { + const relink = node.flags & FLAG_RELINK; + node.value = untrack(buildValue); + if (relink) { + node.flags = FLAG_DIRTY; + refresh(node); + if (node.error) + throw node.error; + } else { + node.flags = FLAG_CLEAN; + } + } + } else { + refresh(node); + if (node.error) + throw node.error; + } + return node.value; + }, + at(index) { + subscribe(); + const key = keys[index]; + return key !== undefined ? signals.get(key) : undefined; + }, + byKey(key) { + subscribe(); + return signals.get(key); + }, + keyAt(index) { + subscribe(); + return keys[index]; + }, + indexOfKey(key) { + subscribe(); + return keys.indexOf(key); + }, + deriveCollection(cb) { + return deriveCollection(collection, cb); + } + }; + return collection; +} +function isCollection(value) { + return isSignalOfType(value, TYPE_COLLECTION); +} +// src/nodes/effect.ts +function createEffect(fn) { + validateCallback("Effect", fn); + const node = { + fn, + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + cleanup: null + }; + const dispose = () => { + runCleanup(node); + node.fn = undefined; + node.flags = FLAG_CLEAN; + node.sourcesTail = null; + trimSources(node); + }; + if (activeOwner) + registerCleanup(activeOwner, dispose); + runEffect(node); + scheduleEffect(node); + return dispose; +} +function match(signalOrSignals, handlers) { + if (!activeOwner) + throw new RequiredOwnerError("match"); + const isSingle = !Array.isArray(signalOrSignals); + const signals = isSingle ? [signalOrSignals] : signalOrSignals; + const { nil, stale } = handlers; + const ok = isSingle ? (values2) => handlers.ok(values2[0]) : (values2) => handlers.ok(values2); + const err = isSingle && handlers.err ? (errors2) => handlers.err(errors2[0]) : handlers.err ?? console.error; + let errors; + let pending = false; + const values = new Array(signals.length); + for (let i = 0;i < signals.length; i++) { + try { + values[i] = signals[i].get(); + } catch (e) { + if (e instanceof UnsetSignalValueError) { + pending = true; + continue; + } + if (!errors) + errors = []; + errors.push(e instanceof Error ? e : new Error(String(e))); + } + } + let out; + try { + if (pending) + out = nil?.(); + else if (errors) + out = err(errors); + else if (stale && (isSingle ? isTask(signals[0]) && signals[0].isPending() : signals.some((s) => isTask(s) && s.isPending()))) + out = stale(); + else + out = ok(values); + } catch (e) { + out = err([e instanceof Error ? e : new Error(String(e))]); + } + if (typeof out === "function") + return out; + if (out instanceof Promise) { + const owner = activeOwner; + const controller = new AbortController; + registerCleanup(owner, () => controller.abort()); + out.then((cleanup) => { + if (!controller.signal.aborted && typeof cleanup === "function") + registerCleanup(owner, cleanup); + }).catch((e) => { + err([e instanceof Error ? e : new Error(String(e))]); + }); + } +} +// src/nodes/sensor.ts +function createSensor(watched, options) { + validateCallback(TYPE_SENSOR, watched, isSyncFunction); + if (options?.value !== undefined) + validateSignalValue(TYPE_SENSOR, options.value, options?.guard); + const node = { + value: options?.value, + sinks: null, + sinksTail: null, + equals: options?.equals ?? DEFAULT_EQUALITY, + guard: options?.guard, + stop: undefined + }; + return { + [Symbol.toStringTag]: TYPE_SENSOR, + get() { + if (activeSink) { + if (!node.sinks) + node.stop = watched((next) => { + validateSignalValue(TYPE_SENSOR, next, node.guard); + setState(node, next); + }); + link(node, activeSink); + } + validateReadValue(TYPE_SENSOR, node.value); + return node.value; + } + }; +} +function isSensor(value) { + return isSignalOfType(value, TYPE_SENSOR); +} +// src/nodes/store.ts +function diffRecords(prev, next) { + const add = {}; + const change = {}; + const remove = {}; + let changed = false; + const prevKeys = Object.keys(prev); + const nextKeys = Object.keys(next); + for (const key of nextKeys) { + if (key in prev) { + if (!DEEP_EQUALITY(prev[key], next[key])) { + change[key] = next[key]; + changed = true; + } + } else { + add[key] = next[key]; + changed = true; + } + } + for (const key of prevKeys) { + if (!(key in next)) { + remove[key] = undefined; + changed = true; + } + } + return { add, change, remove, changed }; +} +function createStore(value, options) { + validateSignalValue(TYPE_STORE, value, isRecord); + const signals = new Map; + const addSignal = (key, val) => { + validateSignalValue(`${TYPE_STORE} for key "${key}"`, val); + if (Array.isArray(val)) + signals.set(key, createList(val)); + else if (isRecord(val)) + signals.set(key, createStore(val)); + else + signals.set(key, createState(val)); + }; + const shapeCategory = (val) => { + if (Array.isArray(val)) + return "list"; + if (isRecord(val)) + return "store"; + return "state"; + }; + const signalCategory = (signal) => { + if (isList(signal)) + return "list"; + if (isStore(signal)) + return "store"; + return "state"; + }; + const buildValue = () => { + const record = {}; + for (const [key, signal] of signals) + record[key] = signal.get(); + return record; + }; + const node = { + fn: buildValue, + value, + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + equals: DEEP_EQUALITY, + error: undefined + }; + const applyChanges = (changes) => { + let structural = false; + for (const key in changes.add) { + addSignal(key, changes.add[key]); + structural = true; + } + let hasChange = false; + for (const _key in changes.change) { + hasChange = true; + break; + } + if (hasChange) { + batch(() => { + for (const key in changes.change) { + const val = changes.change[key]; + validateSignalValue(`${TYPE_STORE} for key "${key}"`, val); + const signal = signals.get(key); + if (signal) { + if (shapeCategory(val) !== signalCategory(signal)) { + addSignal(key, val); + structural = true; + } else + signal.set(val); + } + } + }); + } + for (const key in changes.remove) { + signals.delete(key); + structural = true; + } + if (structural) + node.flags |= FLAG_RELINK; + return changes.changed; + }; + const subscribe = makeSubscribe(node, options?.watched); + for (const key of Object.keys(value)) + addSignal(key, value[key]); + const store = { + [Symbol.toStringTag]: TYPE_STORE, + [Symbol.isConcatSpreadable]: false, + *[Symbol.iterator]() { + subscribe(); + for (const [key, signal] of signals) { + yield [key, signal]; + } + }, + keys() { + subscribe(); + return signals.keys(); + }, + byKey(key) { + return signals.get(key); + }, + get() { + subscribe(); + if (node.sources) { + if (node.flags) { + const relink = node.flags & FLAG_RELINK; + node.value = untrack(buildValue); + if (relink) { + node.flags = FLAG_DIRTY; + refresh(node); + if (node.error) + throw node.error; + } else { + node.flags = FLAG_CLEAN; + } + } + } else { + refresh(node); + if (node.error) + throw node.error; + } + return node.value; + }, + set(next) { + const prev = node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value; + const changes = diffRecords(prev, next); + if (applyChanges(changes)) { + node.flags |= FLAG_DIRTY; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + } + }, + update(fn) { + store.set(fn(untrack(() => store.get()))); + }, + add(key, value2) { + if (signals.has(key)) + throw new DuplicateKeyError(TYPE_STORE, key, value2); + addSignal(key, value2); + node.flags |= FLAG_DIRTY | FLAG_RELINK; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + return key; + }, + remove(key) { + const ok = signals.delete(key); + if (ok) { + node.flags |= FLAG_DIRTY | FLAG_RELINK; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + } + } + }; + return new Proxy(store, { + get(target, prop) { + if (prop in target) + return Reflect.get(target, prop); + if (typeof prop !== "symbol") + return target.byKey(prop); + }, + set(_target, prop) { + throw new InvalidStoreMutationError(String(prop), "assign to"); + }, + deleteProperty(_target, prop) { + throw new InvalidStoreMutationError(String(prop), "delete"); + }, + defineProperty(_target, prop) { + throw new InvalidStoreMutationError(String(prop), "define"); + }, + has(target, prop) { + if (prop in target) + return true; + return target.byKey(String(prop)) !== undefined; + }, + ownKeys(target) { + return Array.from(target.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (prop in target) + return Reflect.getOwnPropertyDescriptor(target, prop); + if (typeof prop === "symbol") + return; + const signal = target.byKey(String(prop)); + return signal ? { + enumerable: true, + configurable: true, + writable: true, + value: signal + } : undefined; + } + }); +} +function isStore(value) { + return isSignalOfType(value, TYPE_STORE); +} + +// src/signal.ts +var SIGNAL_TYPES = new Set([ + TYPE_STATE, + TYPE_MEMO, + TYPE_TASK, + TYPE_SENSOR, + TYPE_SLOT, + TYPE_LIST, + TYPE_COLLECTION, + TYPE_STORE +]); +function createComputed(callback, options) { + return isAsyncFunction(callback) ? createTask(callback, options) : createMemo(callback, options); +} +function createSignal(value) { + if (isSignal(value)) + return value; + if (value == null) + throw new InvalidSignalValueError("createSignal", value); + if (isAsyncFunction(value)) + return createTask(value); + if (isFunction(value)) + return createMemo(value); + if (Array.isArray(value) && value.every((item) => item != null)) + return createList(value); + if (isRecord(value)) + return createStore(value); + return createState(value); +} +function createMutableSignal(value) { + if (isMutableSignal(value)) + return value; + if (value == null || isFunction(value) || isSignal(value)) + throw new InvalidSignalValueError("createMutableSignal", value); + if (Array.isArray(value) && value.every((item) => item != null)) + return createList(value); + if (isRecord(value)) + return createStore(value); + return createState(value); +} +function isComputed(value) { + return isMemo(value) || isTask(value); +} +function isSignal(value) { + return value != null && SIGNAL_TYPES.has(value[Symbol.toStringTag]); +} +function isMutableSignal(value) { + return isState(value) || isStore(value) || isList(value); +} + +// src/nodes/slot.ts +var settingSlots = new WeakSet; +function isSignalOrDescriptor(value) { + if (isSignal(value)) + return true; + return value !== null && typeof value === "object" && "get" in value && typeof value.get === "function"; +} +function createSlot(initialSignal, options) { + validateSignalValue(TYPE_SLOT, initialSignal, isSignalOrDescriptor); + let delegated = initialSignal; + const guard = options?.guard; + const node = { + fn: () => delegated.get(), + value: undefined, + flags: FLAG_DIRTY, + sources: null, + sourcesTail: null, + sinks: null, + sinksTail: null, + equals: options?.equals ?? DEFAULT_EQUALITY, + error: undefined + }; + const get = () => { + if (activeSink) + link(node, activeSink); + refresh(node); + if (node.error) + throw node.error; + return node.value; + }; + const set = (next) => { + if (settingSlots.has(node)) + throw new Error("[Slot] Circular delegation detected in set()"); + settingSlots.add(node); + try { + if (isSlot(delegated)) + return void delegated.set(next); + if ("set" in delegated && typeof delegated.set === "function") { + validateSignalValue(TYPE_SLOT, next, guard); + delegated.set(next); + } else { + throw new ReadonlySignalError(TYPE_SLOT); + } + } finally { + settingSlots.delete(node); + } + }; + const replace = (next) => { + validateSignalValue(TYPE_SLOT, next, isSignalOrDescriptor); + delegated = next; + node.flags |= FLAG_DIRTY; + for (let e = node.sinks;e; e = e.nextSink) + propagate(e.sink); + if (batchDepth === 0) + flush(); + }; + return { + [Symbol.toStringTag]: TYPE_SLOT, + configurable: true, + enumerable: true, + get, + set, + replace, + current: () => delegated + }; +} +function isSlot(value) { + return isSignalOfType(value, TYPE_SLOT); +} +export { + valueString, + untrack, + unown, + match, + isTask, + isStore, + isState, + isSlot, + isSignalOfType, + isSignal, + isSensor, + isRecord, + isObjectOfType, + isMutableSignal, + isMemo, + isList, + isFunction, + isEqual, + isComputed, + isCollection, + isAsyncFunction, + createTask, + createStore, + createState, + createSlot, + createSignal, + createSensor, + createScope, + createMutableSignal, + createMemo, + createList, + createEffect, + createComputed, + createCollection, + batch, + UnsetSignalValueError, + SKIP_EQUALITY, + RequiredOwnerError, + ReadonlySignalError, + PromiseValueError, + NullishSignalValueError, + InvalidStoreMutationError, + InvalidSignalValueError, + InvalidCallbackError, + EffectConvergenceError, + DuplicateKeyError, + DEFAULT_EQUALITY, + DEEP_EQUALITY, + CircularDependencyError +}; diff --git a/index.ts b/index.ts index 7fc4e00..a445922 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ /** * @name Cause & Effect - * @version 1.3.4 + * @version 1.4.0 * @author Esther Brunner */ diff --git a/package.json b/package.json index 104d168..ea91091 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zeix/cause-effect", - "version": "1.3.4", + "version": "1.4.0", "repository": { "type": "git", "url": "https://github.com/zeixcom/cause-effect" @@ -9,10 +9,24 @@ "author": "Esther Brunner", "type": "module", "main": "index.js", - "module": "index.ts", "types": "types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", + "bun": "./index.ts", + "default": "./index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "index.js", + "index.ts", + "src", + "types", + "SECURITY.md" + ], "devDependencies": { - "@biomejs/biome": "2.4.6", + "@biomejs/biome": "^2.5.3", "@types/bun": "^1.3.14", "@zeix/cause-effect-stable": "npm:@zeix/cause-effect", "mitata": "^1.0.34", @@ -22,6 +36,11 @@ "peerDependencies": { "typescript": ">=5.8.0" }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, "description": "Cause & Effect - reactive state management primitives library for TypeScript.", "license": "MIT", "keywords": [ @@ -34,7 +53,7 @@ "access": "public" }, "scripts": { - "build": "bunx tsc --project tsconfig.build.json && bun build index.ts --outdir ./ --minify && bun build index.ts --outfile index.dev.js", + "build": "bunx tsc --project tsconfig.build.json && bun build index.ts --outfile index.js", "bench": "bun run bench/reactivity.bench.ts", "test": "bun test --path-ignore-patterns=**/regression*", "regression": "bun test test/regression-performance.test.ts && bun test test/regression-bundle.test.ts", diff --git a/src/errors.ts b/src/errors.ts index 659a0d2..d67ae29 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -219,19 +219,19 @@ function validateCallback( } export { - type Guard, CircularDependencyError, + DuplicateKeyError, EffectConvergenceError, - NullishSignalValueError, - InvalidSignalValueError, - UnsetSignalValueError, + type Guard, InvalidCallbackError, + InvalidSignalValueError, InvalidStoreMutationError, + NullishSignalValueError, + PromiseValueError, ReadonlySignalError, RequiredOwnerError, - DuplicateKeyError, - PromiseValueError, - validateSignalValue, - validateReadValue, + UnsetSignalValueError, validateCallback, + validateReadValue, + validateSignalValue, } diff --git a/src/graph.ts b/src/graph.ts index 12f2b25..e2a449e 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -793,53 +793,53 @@ function makeSubscribe(node: SourceNode, onWatch?: () => Cleanup): () => void { } export { - type Cleanup, - type ComputedOptions, - type EffectCallback, - type EffectNode, - type MaybeCleanup, - type MemoCallback, - type MemoNode, - type Scope, - type ScopeOptions, - type Signal, - type SignalOptions, - type SinkNode, - type StateNode, - type TaskCallback, - type TaskNode, activeOwner, activeSink, batch, batchDepth, + type Cleanup, + type ComputedOptions, createScope, - DEFAULT_EQUALITY, DEEP_EQUALITY, - isEqual, - SKIP_EQUALITY, + DEFAULT_EQUALITY, + type EffectCallback, + type EffectNode, FLAG_CHECK, FLAG_CLEAN, FLAG_DIRTY, FLAG_RELINK, flush, + isEqual, link, + type MaybeCleanup, + type MemoCallback, + type MemoNode, makeSubscribe, propagate, refresh, registerCleanup, runCleanup, runEffect, + type Scope, + type ScopeOptions, + type Signal, + type SignalOptions, + type SinkNode, + SKIP_EQUALITY, + type StateNode, scheduleEffect, setState, - trimSources, + type TaskCallback, + type TaskNode, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, - TYPE_STATE, TYPE_SLOT, + TYPE_STATE, TYPE_STORE, TYPE_TASK, + trimSources, unlink, unown, untrack, diff --git a/src/nodes/collection.ts b/src/nodes/collection.ts index 00cc4f3..5610c1a 100644 --- a/src/nodes/collection.ts +++ b/src/nodes/collection.ts @@ -573,13 +573,13 @@ function isCollection = Signal>( /* === Exports === */ export { - createCollection, - deriveCollection, - isCollection, type Collection, type CollectionCallback, type CollectionChanges, type CollectionOptions, type CollectionSource, + createCollection, type DeriveCollectionCallback, + deriveCollection, + isCollection, } diff --git a/src/nodes/effect.ts b/src/nodes/effect.ts index 028179a..01e9a7a 100644 --- a/src/nodes/effect.ts +++ b/src/nodes/effect.ts @@ -235,9 +235,9 @@ function match( } export { - type MaybePromise, - type MatchHandlers, - type SingleMatchHandlers, createEffect, + type MatchHandlers, + type MaybePromise, match, + type SingleMatchHandlers, } diff --git a/src/nodes/list.ts b/src/nodes/list.ts index 68bc72b..29618bf 100644 --- a/src/nodes/list.ts +++ b/src/nodes/list.ts @@ -643,14 +643,14 @@ function isList = MutableSignal>( /* === Exports === */ export { + createList, type DiffResult, + getKeyGenerator, + isList, type KeyConfig, + keysEqual, type List, type ListOptions, - type UnknownRecord, - createList, - isList, - getKeyGenerator, - keysEqual, TYPE_LIST, + type UnknownRecord, } diff --git a/src/signal.ts b/src/signal.ts index 21607e0..485d44e 100644 --- a/src/signal.ts +++ b/src/signal.ts @@ -158,11 +158,11 @@ function isMutableSignal(value: unknown): value is MutableSignal { } export { - type MutableSignal, createComputed, - createSignal, createMutableSignal, + createSignal, isComputed, - isSignal, isMutableSignal, + isSignal, + type MutableSignal, } diff --git a/src/util.ts b/src/util.ts index ac7df23..af0d146 100644 --- a/src/util.ts +++ b/src/util.ts @@ -66,11 +66,11 @@ function valueString(value: unknown): string { /* === Exports === */ export { - isFunction, isAsyncFunction, - isSyncFunction, + isFunction, isObjectOfType, - isSignalOfType, isRecord, + isSignalOfType, + isSyncFunction, valueString, }