@cplieger/reactive 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +3 -0
- package/README.md +5 -6
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/jsr.json +3 -1
- package/package.json +20 -12
- package/src/reconcile.ts +24 -8
- package/src/signal.ts +11 -3
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
# reactive
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@cplieger/reactive)
|
|
4
|
-
[](https://jsr.io/@cplieger/reactive)
|
|
5
|
-
[](https://github.com/cplieger/reactive/actions/workflows/coverage.yml)
|
|
6
|
-
[](https://github.com/cplieger/reactive/issues?q=label%3Astryker-tracker)
|
|
7
|
-
[](https://www.bestpractices.dev/projects/13218)
|
|
8
|
-
[](https://scorecard.dev/viewer/?uri=github.com/cplieger/reactive)
|
|
3
|
+
[](https://www.npmjs.com/package/@cplieger/reactive) [](https://jsr.io/@cplieger/reactive) [](https://github.com/cplieger/reactive/issues?q=label%3Astryker-tracker)
|
|
9
4
|
|
|
10
5
|
> Signals + collections + DOM-reconciliation micro-framework for TypeScript
|
|
11
6
|
|
|
@@ -23,6 +18,8 @@ A standalone reactive app-skeleton:
|
|
|
23
18
|
|
|
24
19
|
The store, `SignalMap`, and `Collection` are thin facades over a single signal engine: one reactive core, not several. Zero dependencies beyond the DOM API.
|
|
25
20
|
|
|
21
|
+
> **What needs a DOM:** the render half, `el`, `bindList`, `reconcile`, `patch` and `trackHandler`. The engine and the state tiers do not: `signal`, `effect`, `computed`, `batch`, `untracked`, `createStore`, `SignalMap`, `createCollection` and `createBus`. Nothing touches the DOM at import time, so importing the package under Node, Deno, Bun or Workers succeeds and only the DOM functions throw when called.
|
|
22
|
+
|
|
26
23
|
Mirrors semantics from @preact/signals-core and solid-js reactivity.
|
|
27
24
|
|
|
28
25
|
## Flush model
|
|
@@ -202,3 +199,5 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
|
|
|
202
199
|
## License
|
|
203
200
|
|
|
204
201
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
202
|
+
|
|
203
|
+
Third-party attributions are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
No third-party code is included in this repository. The signal engine is a re-derivation of an upstream design, not a copy of its source, so no license text is reproduced here.
|
|
4
|
+
|
|
5
|
+
Two designs are followed, each named at the line of ours that follows it:
|
|
6
|
+
|
|
7
|
+
- The dependency graph and the scheduler in `src/signal.ts` follow [@preact/signals-core](https://github.com/preactjs/signals) (MIT): the doubly-linked source/target edge nodes (`DepNode`, `SourceNode`, `TargetNode` at `src/signal.ts:75-100`), the pull-based glitch-free `refreshComputed` (`src/signal.ts:327`), the eager drain in `endBatch` (`src/signal.ts:397`), and the untracked `subscribe` callback (`src/signal.ts:711`, upstream `preactjs/signals#188`). The pinned baseline is `@preact/signals-core@1.14.4`; the header of `src/signal.ts` records every deliberate delta from it and the upstream machinery this engine does not carry, and `CONTRIBUTING.md` records the drift-audit procedure.
|
|
8
|
+
- The explicit dependency helper `on()` (`src/signal.ts:734`) follows [solid-js](https://github.com/solidjs/solid) (MIT), whose `on()` has the same deps-plus-body shape; `untracked()` (`src/signal.ts:666`) is Solid's `untrack()` under the name Preact gives it.
|
package/jsr.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/reactive",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"publish": {
|
|
9
9
|
"include": [
|
|
10
10
|
"LICENSE",
|
|
11
|
+
"NOTICE",
|
|
11
12
|
"README.md",
|
|
13
|
+
"THIRD_PARTY_NOTICES.md",
|
|
12
14
|
"src/**/*.ts"
|
|
13
15
|
],
|
|
14
16
|
"exclude": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/reactive",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"typecheck": "tsc -p tsconfig.json",
|
|
11
11
|
"typecheck:tests": "tsc -p tsconfig.tests.json",
|
|
12
|
-
"test": "vitest --run"
|
|
12
|
+
"test": "vitest --run",
|
|
13
|
+
"lint:eslint": "eslint .",
|
|
14
|
+
"lint:prettier": "prettier --check .",
|
|
15
|
+
"lint:knip": "knip",
|
|
16
|
+
"lint:fix": "eslint . --fix && prettier --write ."
|
|
13
17
|
},
|
|
14
18
|
"devDependencies": {
|
|
15
19
|
"@eslint/js": "10.0.1",
|
|
@@ -17,16 +21,17 @@
|
|
|
17
21
|
"@stryker-mutator/typescript-checker": "10.0.0",
|
|
18
22
|
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
19
23
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
20
|
-
"@vitest/browser": "
|
|
21
|
-
"@vitest/browser-playwright": "
|
|
22
|
-
"@vitest/coverage-v8": "
|
|
23
|
-
"eslint": "10.
|
|
24
|
-
"fast-check": "4.
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
"@vitest/browser": "5.0.1",
|
|
25
|
+
"@vitest/browser-playwright": "5.0.1",
|
|
26
|
+
"@vitest/coverage-v8": "5.0.1",
|
|
27
|
+
"eslint": "10.10.0",
|
|
28
|
+
"fast-check": "4.10.1",
|
|
29
|
+
"knip": "6.36.0",
|
|
30
|
+
"playwright": "1.63.0",
|
|
31
|
+
"prettier": "3.9.7",
|
|
27
32
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
28
|
-
"typescript-eslint": "8.
|
|
29
|
-
"vitest": "
|
|
33
|
+
"typescript-eslint": "8.70.0",
|
|
34
|
+
"vitest": "5.0.1"
|
|
30
35
|
},
|
|
31
36
|
"overrides": {
|
|
32
37
|
"qs": "^6.15.2"
|
|
@@ -61,6 +66,9 @@
|
|
|
61
66
|
"files": [
|
|
62
67
|
"src/",
|
|
63
68
|
"jsr.json",
|
|
64
|
-
"!src/**/*.test.ts"
|
|
69
|
+
"!src/**/*.test.ts",
|
|
70
|
+
"LICENSE",
|
|
71
|
+
"NOTICE",
|
|
72
|
+
"THIRD_PARTY_NOTICES.md"
|
|
65
73
|
]
|
|
66
74
|
}
|
package/src/reconcile.ts
CHANGED
|
@@ -30,10 +30,31 @@ export function reconcile<T>(
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// DEPARTING ELEMENTS GO FIRST, before anything is placed. The walk below runs
|
|
34
|
+
// backwards and skips an element already sitting before the last one it placed; a
|
|
35
|
+
// departing element still in the tree makes every PREDECESSOR's `nextSibling` point at
|
|
36
|
+
// a node about to vanish, so the guard sees a mismatch and re-seats a row nothing
|
|
37
|
+
// changed — which restarts its animations and drops `:hover` and focus. So `onRemove`
|
|
38
|
+
// fires before the survivors are placed, holding its own intact element; only the
|
|
39
|
+
// siblings' positions are not yet final. `key` is still called exactly once per item,
|
|
40
|
+
// and the pairs carry it into the walk so neither loop indexes an array.
|
|
41
|
+
const pairs: [T, string][] = [];
|
|
42
|
+
const wanted = new Set<string>();
|
|
43
|
+
for (const item of items) {
|
|
36
44
|
const k = spec.key(item);
|
|
45
|
+
pairs.push([item, k]);
|
|
46
|
+
wanted.add(k);
|
|
47
|
+
}
|
|
48
|
+
for (const [k, el] of existing) {
|
|
49
|
+
if (!wanted.has(k)) {
|
|
50
|
+
spec.onRemove?.(el, k);
|
|
51
|
+
el.remove();
|
|
52
|
+
existing.delete(k);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let target: Node | null = null;
|
|
57
|
+
for (const [item, k] of pairs.reverse()) {
|
|
37
58
|
let el = existing.get(k);
|
|
38
59
|
if (el === undefined) {
|
|
39
60
|
el = spec.mount(item);
|
|
@@ -52,9 +73,4 @@ export function reconcile<T>(
|
|
|
52
73
|
}
|
|
53
74
|
target = el;
|
|
54
75
|
}
|
|
55
|
-
|
|
56
|
-
for (const [k, el] of existing) {
|
|
57
|
-
spec.onRemove?.(el, k);
|
|
58
|
-
el.remove();
|
|
59
|
-
}
|
|
60
76
|
}
|
package/src/signal.ts
CHANGED
|
@@ -746,19 +746,27 @@ export function on<T, U>(
|
|
|
746
746
|
fn: (value: T, prev: T | undefined, prevResult: U | undefined) => U,
|
|
747
747
|
options?: { defer?: false },
|
|
748
748
|
): () => U;
|
|
749
|
-
|
|
749
|
+
/** Single accessor, deferred: the first call returns undefined. A `defer`
|
|
750
|
+
* typed as `boolean` selects this overload even when the value is `false`,
|
|
751
|
+
* because the type cannot rule the deferred call out; pass a literal
|
|
752
|
+
* `false` (or omit `options`) to keep the non-undefined return. */
|
|
750
753
|
export function on<T, U>(
|
|
751
754
|
deps: () => T,
|
|
752
755
|
fn: (value: T, prev: T | undefined, prevResult: U | undefined) => U,
|
|
753
756
|
options: { defer: boolean },
|
|
754
757
|
): () => U | undefined;
|
|
755
|
-
|
|
758
|
+
/** Array of accessors, non-deferred (or explicit `{ defer: false }`): never
|
|
759
|
+
* undefined. `value` and `prev` are the accessors' results in declaration
|
|
760
|
+
* order, typed `unknown[]` — a heterogeneous list is not correlated
|
|
761
|
+
* positionally, so narrow each element before use. */
|
|
756
762
|
export function on<U>(
|
|
757
763
|
deps: (() => unknown)[],
|
|
758
764
|
fn: (value: unknown[], prev: unknown[] | undefined, prevResult: U | undefined) => U,
|
|
759
765
|
options?: { defer?: false },
|
|
760
766
|
): () => U;
|
|
761
|
-
|
|
767
|
+
/** Array of accessors, deferred: the first call returns undefined, having read
|
|
768
|
+
* every accessor to seed `prev`. Same `boolean`-widening caveat as the single
|
|
769
|
+
* deferred overload. */
|
|
762
770
|
export function on<U>(
|
|
763
771
|
deps: (() => unknown)[],
|
|
764
772
|
fn: (value: unknown[], prev: unknown[] | undefined, prevResult: U | undefined) => U,
|