solid-deps 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +202 -2
- package/package.json +47 -4
- package/src/attach-read.mjs +175 -0
- package/src/index.mjs +1 -0
- package/types/index.d.mts +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 solid-deps contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,203 @@
|
|
|
1
|
-
#
|
|
1
|
+
# solid-deps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Attach additional reactive dependencies to an existing **Solid 2 effect**—even after an `await`—without re-running the effect at the moment of attachment.
|
|
4
|
+
|
|
5
|
+
`solid-deps` is an experimental, browser-oriented utility for Solid 2. It lets a synchronous callback read signals, memos, or store properties and registers those reads as dependencies of an existing tracked computation. When an attached dependency changes, Solid can rerun that computation normally.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install solid-deps@0.0.2 solid-js@2.0.0-rc.17 @solidjs/signals@2.0.0-rc.17
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
With Bun:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bun add solid-deps@0.0.2 solid-js@2.0.0-rc.17 @solidjs/signals@2.0.0-rc.17
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Compatibility:** This release targets **Solid 2.0.0-rc.17** (`solid-js` and `@solidjs/signals`). Both the app and `solid-deps` must resolve to the **same instance** of `@solidjs/signals`. Server-side rendering is not supported. See [Limitations](#limitations).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Capture the computation before `await`, then attach the reads after the asynchronous work finishes.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { createEffect, createSignal, getOwner } from "solid-js";
|
|
27
|
+
import { attachRead, captureObserverPass } from "solid-deps";
|
|
28
|
+
|
|
29
|
+
export default function Counter() {
|
|
30
|
+
const [count, setCount] = createSignal(0);
|
|
31
|
+
|
|
32
|
+
createEffect(
|
|
33
|
+
async () => {
|
|
34
|
+
// Capture the current effect pass while it is still tracking.
|
|
35
|
+
const pass = captureObserverPass(getOwner());
|
|
36
|
+
|
|
37
|
+
await Promise.resolve();
|
|
38
|
+
|
|
39
|
+
// count() becomes a dependency of this effect.
|
|
40
|
+
const [value, result] = attachRead(pass, () => count());
|
|
41
|
+
|
|
42
|
+
// An older async run may finish after a newer one, or after disposal.
|
|
43
|
+
if (result.stale || result.disposed) return;
|
|
44
|
+
|
|
45
|
+
console.log("Count:", value);
|
|
46
|
+
},
|
|
47
|
+
() => {}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<button type="button" onClick={() => setCount(value => value + 1)}>
|
|
52
|
+
Increment: {count()}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The first late read adds `count` as a dependency. Later changes to `count` can rerun the effect; each new pass captures its own token and reattaches the read after `await`.
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
The package exports `attachRead`, `captureObserverPass`, and `inspectDeps` from its main entry point.
|
|
63
|
+
|
|
64
|
+
### `attachRead(observerOrPass, read)`
|
|
65
|
+
|
|
66
|
+
Runs a **synchronous** reader and attaches the reactive sources it accesses to the specified existing computation.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const [retValue, result] = attachRead(pass, () => {
|
|
70
|
+
const countValue = count();
|
|
71
|
+
const nameValue = name();
|
|
72
|
+
return { countValue, nameValue };
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The return value is always a tuple:
|
|
77
|
+
|
|
78
|
+
- **`retValue`** — the exact value returned by `read()`, or `undefined` if the read was skipped.
|
|
79
|
+
- **`result`** — an `AttachReadResult` object describing the attachment.
|
|
80
|
+
|
|
81
|
+
| Result field | Type | Meaning |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `added` | `number` | Newly attached or revalidated dependencies, including promoted ones. |
|
|
84
|
+
| `duplicate` | `number` | Dependencies already valid for the current effect pass. |
|
|
85
|
+
| `promoted` | `number` | Previously attached dependencies reused for this pass; included in `added`. |
|
|
86
|
+
| `disposed` | `boolean` | Target computation was disposed; `read()` was not called. |
|
|
87
|
+
| `stale` | `boolean` | Captured pass has been superseded; `read()` was not called. |
|
|
88
|
+
|
|
89
|
+
A disposed or stale pass returns `[undefined, result]` without executing `read()`. Errors thrown by the reader propagate to the caller. A reader returning a Promise or thenable is rejected with a `TypeError`; awaiting work **before** calling `attachRead()` is supported, but passing an `async` reader is not.
|
|
90
|
+
|
|
91
|
+
You may also pass a captured computation directly:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { getOwner } from "solid-js";
|
|
95
|
+
|
|
96
|
+
// `owner` must have been captured inside a tracked computation.
|
|
97
|
+
const [value, result] = attachRead(owner, () => count());
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For async effects, prefer a pass token so an older continuation cannot attach dependencies to a newer execution.
|
|
101
|
+
|
|
102
|
+
### `captureObserverPass(observer)`
|
|
103
|
+
|
|
104
|
+
Captures a token identifying the current execution of a tracked computation. Call it **during the computation's tracked phase, before an `await`**.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const pass = captureObserverPass(getOwner());
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pass the token to `attachRead()`. If a newer execution has started, the older token produces `result.stale === true` and the reader is skipped.
|
|
111
|
+
|
|
112
|
+
`getOwner()` must refer to an actual tracking computation. An ordinary component/root owner is not a suitable attachment target.
|
|
113
|
+
|
|
114
|
+
### `inspectDeps(observer)`
|
|
115
|
+
|
|
116
|
+
Returns an array of the current dependency **source objects** for the supplied computation:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const sources = inspectDeps(owner);
|
|
120
|
+
console.log(sources.length);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is a diagnostic helper, not an accessor-value API. Treat returned source objects as read-only.
|
|
124
|
+
|
|
125
|
+
## TypeScript
|
|
126
|
+
|
|
127
|
+
Type declarations are included. No separate `@types` package is needed.
|
|
128
|
+
|
|
129
|
+
The package exports the following types and typed function signatures:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import type { Owner } from "@solidjs/signals";
|
|
133
|
+
|
|
134
|
+
export interface ObserverPass {
|
|
135
|
+
readonly __solidDepsObserverPass: unique symbol;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface AttachReadResult {
|
|
139
|
+
readonly added: number;
|
|
140
|
+
readonly duplicate: number;
|
|
141
|
+
readonly promoted: number;
|
|
142
|
+
readonly disposed: boolean;
|
|
143
|
+
readonly stale: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export declare function captureObserverPass(observer: Owner | null): ObserverPass;
|
|
147
|
+
|
|
148
|
+
export declare function attachRead<T>(
|
|
149
|
+
observerOrPass: Owner | ObserverPass | null,
|
|
150
|
+
read: () => T
|
|
151
|
+
): [retValue: T | undefined, result: AttachReadResult];
|
|
152
|
+
|
|
153
|
+
export declare function inspectDeps(observer: Owner): unknown[];
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For example, TypeScript preserves the reader's return type:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { createEffect, createSignal, getOwner } from "solid-js";
|
|
160
|
+
import { attachRead, captureObserverPass } from "solid-deps";
|
|
161
|
+
import type { AttachReadResult, ObserverPass } from "solid-deps";
|
|
162
|
+
|
|
163
|
+
const [count] = createSignal(0);
|
|
164
|
+
|
|
165
|
+
createEffect(async () => {
|
|
166
|
+
const pass: ObserverPass = captureObserverPass(getOwner());
|
|
167
|
+
await Promise.resolve();
|
|
168
|
+
|
|
169
|
+
const [value, result] = attachRead(pass, () => count());
|
|
170
|
+
// value: number | undefined
|
|
171
|
+
// result: AttachReadResult
|
|
172
|
+
|
|
173
|
+
if (!result.stale && !result.disposed && value !== undefined) {
|
|
174
|
+
console.log(value.toFixed(0));
|
|
175
|
+
}
|
|
176
|
+
}, () => {});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`ObserverPass` is an opaque type. Obtain one from `captureObserverPass()` rather than constructing it yourself.
|
|
180
|
+
|
|
181
|
+
## Dependency lifetime
|
|
182
|
+
|
|
183
|
+
- Reads are attached **without immediately re-running** the target computation.
|
|
184
|
+
- Source changes that occurred **before** attachment are not replayed.
|
|
185
|
+
- The attached dependencies participate in subsequent normal updates. On the next computation pass, Solid may remove dependencies that are not read or reattached for that pass.
|
|
186
|
+
- Use `captureObserverPass()` for async work that may overlap with newer effect executions.
|
|
187
|
+
- Disposed computations cannot receive new dependencies.
|
|
188
|
+
|
|
189
|
+
## Limitations
|
|
190
|
+
|
|
191
|
+
`solid-deps` is experimental and restricted to Solid **2.0.0-rc.17** in browser-oriented applications. Dependency registration requires an existing **tracked computation**, not simply any owner.
|
|
192
|
+
|
|
193
|
+
Keep `read()` synchronous and limited to settled reactive reads, such as signals, memos, or store properties. Do not use it to create computations, install cleanups, write signals, or perform side effects. It does not run in the target's original reactive context. The library rejects reader callbacks that create owned children or register cleanups, and rejects sources owned by its temporary observer, to prevent subscriptions to disposed computations.
|
|
194
|
+
|
|
195
|
+
Pending async memo reads can throw `NotReadyError`; a failed read does not attach dependencies. A memo with a `loadingValue` can return that fallback while pending, but `attachRead` does not restore the target observer's original suspension or transition context. Treat resources, loading boundaries, held transactions, and optimistic state as **unsupported beyond ordinary settled reads**.
|
|
196
|
+
|
|
197
|
+
An `attachRead()` subscription is prospective: signal updates that happen before attachment are not replayed. Grafted dependencies can be removed by the target's next normal recomputation. For async effects, reattach on each pass using `captureObserverPass()`.
|
|
198
|
+
|
|
199
|
+
Reads that create computations/cleanups, write signals, or depend on `getOwner()`/`getObserver()` seeing the original effect are unsupported, because the callback executes under a temporary observer. Server rendering and independently bundled copies of the signals runtime are not supported. Do not assume compatibility with other Solid versions.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-deps",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Experimental direct dependency attachment for Solid 2 reactive observers after async boundaries",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./types/index.d.mts",
|
|
13
|
+
"default": "./src/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"types": "./types/index.d.mts",
|
|
17
|
+
"files": [
|
|
18
|
+
"src/attach-read.mjs",
|
|
19
|
+
"src/index.mjs",
|
|
20
|
+
"types/index.d.mts",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@solidjs/signals": "2.0.0-rc.17"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"solid",
|
|
30
|
+
"solidjs",
|
|
31
|
+
"signals",
|
|
32
|
+
"reactivity",
|
|
33
|
+
"async",
|
|
34
|
+
"observer",
|
|
35
|
+
"dependency-tracking"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"setup": "node scripts/setup.mjs",
|
|
39
|
+
"test": "node --conditions=browser --test test/test.mjs test/test-async.mjs test/test-tuple.mjs test/test-manifest.mjs test/test-graph-hardening.mjs test/test-async-transitions.mjs test/test-transaction-integrity.mjs test/test-scheduler-lifecycle.mjs test/test-release-hardening.mjs",
|
|
40
|
+
"test:dev": "node --conditions=browser --conditions=development --test test/test.mjs test/test-async.mjs test/test-tuple.mjs test/test-manifest.mjs test/test-graph-hardening.mjs test/test-async-transitions.mjs test/test-transaction-integrity.mjs test/test-scheduler-lifecycle.mjs test/test-release-hardening.mjs",
|
|
41
|
+
"test:observe": "node --conditions=browser --conditions=observe --test test/test.mjs test/test-async.mjs test/test-tuple.mjs test/test-manifest.mjs test/test-graph-hardening.mjs test/test-async-transitions.mjs test/test-transaction-integrity.mjs test/test-scheduler-lifecycle.mjs test/test-release-hardening.mjs",
|
|
42
|
+
"test:all": "node scripts/run-tests.mjs",
|
|
43
|
+
"test:package": "node scripts/smoke-package.mjs"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@solidjs/signals": "2.0.0-rc.17",
|
|
47
|
+
"solid-js": "2.0.0-rc.17"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental RC.17 graph surgery. No changes/forks of Solid.
|
|
3
|
+
* Target must be captured using getObserver() while its tracking function runs.
|
|
4
|
+
* Scope: synchronous, settled signal and memo reads, outside a target recompute.
|
|
5
|
+
* Maintenance/compatibility: see HANDOFF.md and docs/INTERNALS.md.
|
|
6
|
+
* This is link transfer, NOT restoration of the original observer context.
|
|
7
|
+
*/
|
|
8
|
+
import { createRoot, createReaction, getObserver, isDisposed } from '@solidjs/signals';
|
|
9
|
+
|
|
10
|
+
const layouts = {
|
|
11
|
+
dev: { deps: '_deps', tail: '_depsTail', gen: '_depGen', flags: '_flags', dep: '_dep', sub: '_sub', next: '_nextDep', linkGen: '_gen', child: '_firstChild', disposal: '_disposal' },
|
|
12
|
+
prod: { deps: 'C', tail: '_', gen: 'Ne', flags: 'R', dep: 'S', sub: 'u', next: 'G', linkGen: 'Te', child: 'De', disposal: 'He' },
|
|
13
|
+
observe: { deps: 'C', tail: '_', gen: 'Ne', flags: 'R', dep: 'S', sub: 'u', next: 'G', linkGen: 'Te', child: 'ze', disposal: 'rt' }
|
|
14
|
+
};
|
|
15
|
+
function layoutOf(node) {
|
|
16
|
+
const shape = node && (('_deps' in node) ? layouts.dev : ('C' in node) ? ('ze' in node ? layouts.observe : layouts.prod) : null);
|
|
17
|
+
if (!shape || typeof node[shape.gen] !== 'number' || !(shape.tail in node))
|
|
18
|
+
throw new TypeError('Expected a tracking Computed from @solidjs/signals rc.17, not a plain Owner');
|
|
19
|
+
return shape;
|
|
20
|
+
}
|
|
21
|
+
// Probe-owned sources disappear when the temporary tracking root is disposed.
|
|
22
|
+
// Their dependency edges must never be transferred to a longer-lived target.
|
|
23
|
+
function ownedBy(node, ancestor) {
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (let owner = node; owner != null; owner = owner._parent) {
|
|
26
|
+
if (owner === ancestor) return true;
|
|
27
|
+
if (seen.has(owner)) throw new Error('Cyclic owner ancestry');
|
|
28
|
+
seen.add(owner);
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function links(owner, layout) {
|
|
33
|
+
const out = [];
|
|
34
|
+
let cur = owner[layout.deps];
|
|
35
|
+
const visited = new Set();
|
|
36
|
+
while (cur != null) {
|
|
37
|
+
if (visited.has(cur)) throw new Error('Cyclic dependency list');
|
|
38
|
+
visited.add(cur);
|
|
39
|
+
out.push(cur);
|
|
40
|
+
cur = cur[layout.next];
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
// Capture a generation before an await, so a superseded async invocation
|
|
45
|
+
// cannot attach subscriptions to a more recent pass of the same effect.
|
|
46
|
+
const observerPasses = new WeakMap();
|
|
47
|
+
export function captureObserverPass(observer) {
|
|
48
|
+
const shape = layoutOf(observer);
|
|
49
|
+
const token = Object.freeze({});
|
|
50
|
+
observerPasses.set(token, { observer, generation: observer[shape.gen] });
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Attach dependencies read by `read` onto `target` without re-running target.
|
|
56
|
+
* Returns [retValue, result] where result describes the graft.
|
|
57
|
+
* They remain until target recomputes and performs normal dependency reconciliation.
|
|
58
|
+
*/
|
|
59
|
+
export function attachRead(observerOrPass, read) {
|
|
60
|
+
const pass = observerOrPass && observerPasses.get(observerOrPass);
|
|
61
|
+
const target = pass ? pass.observer : observerOrPass;
|
|
62
|
+
const s = layoutOf(target);
|
|
63
|
+
if (typeof read !== 'function') throw new TypeError('read must be a function');
|
|
64
|
+
if (isDisposed(target)) return [undefined, { added: 0, duplicate: 0, promoted: 0, disposed: true, stale: false }];
|
|
65
|
+
if (pass && target[s.gen] !== pass.generation)
|
|
66
|
+
return [undefined, { added: 0, duplicate: 0, promoted: 0, disposed: false, stale: true }];
|
|
67
|
+
if (target[s.flags] & 4) throw new Error('Target observer is currently recomputing');
|
|
68
|
+
const existing = links(target, s);
|
|
69
|
+
const beforeTail = target[s.tail];
|
|
70
|
+
const beforeGen = target[s.gen];
|
|
71
|
+
// Solid can retain an uncommitted stale suffix from a previous pass.
|
|
72
|
+
// The tail identifies the validated *prefix*, not necessarily the list end.
|
|
73
|
+
const prefixEnd = beforeTail == null ? -1 : existing.indexOf(beforeTail);
|
|
74
|
+
if (beforeTail !== null && prefixEnd < 0)
|
|
75
|
+
throw new Error('Target tail is not reachable from its dependency head');
|
|
76
|
+
const valid = existing.slice(0, prefixEnd + 1);
|
|
77
|
+
const stale = existing.slice(prefixEnd + 1);
|
|
78
|
+
let disposeRoot;
|
|
79
|
+
let probe;
|
|
80
|
+
let caught;
|
|
81
|
+
let retValue;
|
|
82
|
+
try {
|
|
83
|
+
createRoot(dispose => {
|
|
84
|
+
disposeRoot = dispose;
|
|
85
|
+
const track = createReaction(() => {});
|
|
86
|
+
track(() => {
|
|
87
|
+
probe = getObserver();
|
|
88
|
+
try { retValue = read(); } catch (err) { caught = [err]; }
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
if (caught) throw caught[0];
|
|
92
|
+
if (retValue != null && typeof retValue.then === 'function')
|
|
93
|
+
throw new TypeError('attachRead requires synchronous reads, not an async callback');
|
|
94
|
+
if (!probe) throw new Error('No temporary observer was captured');
|
|
95
|
+
const p = layoutOf(probe);
|
|
96
|
+
if (p !== s) throw new Error('Temporary observer and target use different Solid builds');
|
|
97
|
+
if (isDisposed(target)) return [undefined, { added: 0, duplicate: 0, promoted: 0, disposed: true, stale: false }];
|
|
98
|
+
if ((target[s.flags] & 4) || target[s.tail] !== beforeTail || target[s.gen] !== beforeGen ||
|
|
99
|
+
(target[s.deps] ?? null) !== (existing[0] ?? null))
|
|
100
|
+
throw new Error('Target changed during capture; refusing unsafe graft');
|
|
101
|
+
const after = links(target, s);
|
|
102
|
+
if (after.length !== existing.length || after.some((edge, i) => edge !== existing[i]))
|
|
103
|
+
throw new Error('Target dependency chain changed during capture');
|
|
104
|
+
|
|
105
|
+
const captured = links(probe, s);
|
|
106
|
+
// `read()` must not allocate owned children or register cleanups. A child
|
|
107
|
+
// can disappear when the probe is disposed, and a cleanup can throw AFTER
|
|
108
|
+
// a successful graft, leaving the target changed despite a failed call.
|
|
109
|
+
if (probe[s.child] != null || probe[s.disposal] != null)
|
|
110
|
+
throw new Error('attachRead read() cannot create owned children or register cleanups');
|
|
111
|
+
// Guard before touching either graph. A memo or signal created inside
|
|
112
|
+
// `read()` may be owned by the disposable probe, and disposing that probe
|
|
113
|
+
// would otherwise leave the target subscribed to a disposed source.
|
|
114
|
+
for (const edge of captured) {
|
|
115
|
+
const source = edge[s.dep];
|
|
116
|
+
if (isDisposed(source) || ownedBy(source, probe))
|
|
117
|
+
throw new Error('Cannot attach a disposed or temporary-observer-owned source');
|
|
118
|
+
}
|
|
119
|
+
const seen = new Set(valid.map(edge => edge[s.dep]));
|
|
120
|
+
const staleBySource = new Map(stale.map(edge => [edge[s.dep], edge]));
|
|
121
|
+
const moved = [];
|
|
122
|
+
const promoted = [];
|
|
123
|
+
const kept = [];
|
|
124
|
+
|
|
125
|
+
for (const edge of captured) {
|
|
126
|
+
const source = edge[s.dep];
|
|
127
|
+
if (seen.has(source)) {
|
|
128
|
+
kept.push(edge);
|
|
129
|
+
} else {
|
|
130
|
+
seen.add(source);
|
|
131
|
+
const old = staleBySource.get(source);
|
|
132
|
+
if (old) {
|
|
133
|
+
promoted.push(old);
|
|
134
|
+
kept.push(edge);
|
|
135
|
+
} else {
|
|
136
|
+
moved.push(edge);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Fix the temporary observer's list before its disposer walks it.
|
|
141
|
+
probe[s.deps] = kept[0] ?? null;
|
|
142
|
+
probe[s.tail] = kept.at(-1) ?? null;
|
|
143
|
+
for (let i = 0; i < kept.length; i++) kept[i][s.next] = kept[i+1] ?? null;
|
|
144
|
+
|
|
145
|
+
// Rebuild the target's list as:
|
|
146
|
+
// validated prefix -> newly validated links -> stale suffix.
|
|
147
|
+
// Trimming the suffix remains Solid's responsibility at commit/settle.
|
|
148
|
+
const promotedSet = new Set(promoted);
|
|
149
|
+
const staleRemainder = stale.filter(edge => !promotedSet.has(edge));
|
|
150
|
+
const additions = [...promoted, ...moved];
|
|
151
|
+
const validated = [...valid, ...additions];
|
|
152
|
+
const nextList = [...validated, ...staleRemainder];
|
|
153
|
+
for (const edge of additions) {
|
|
154
|
+
edge[s.sub] = target;
|
|
155
|
+
edge[s.linkGen] = beforeGen;
|
|
156
|
+
}
|
|
157
|
+
for (let i = 0; i < nextList.length; i++)
|
|
158
|
+
nextList[i][s.next] = nextList[i + 1] ?? null;
|
|
159
|
+
target[s.deps] = nextList[0] ?? null;
|
|
160
|
+
target[s.tail] = validated.at(-1) ?? null;
|
|
161
|
+
return [retValue, {
|
|
162
|
+
added: additions.length,
|
|
163
|
+
duplicate: kept.length - promoted.length,
|
|
164
|
+
promoted: promoted.length,
|
|
165
|
+
disposed: false,
|
|
166
|
+
stale: false
|
|
167
|
+
}];
|
|
168
|
+
} finally {
|
|
169
|
+
disposeRoot?.();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function inspectDeps(owner) {
|
|
173
|
+
const s = layoutOf(owner);
|
|
174
|
+
return links(owner, s).map(link => link[s.dep]);
|
|
175
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { attachRead, captureObserverPass, inspectDeps } from './attach-read.mjs';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Owner } from '@solidjs/signals';
|
|
2
|
+
|
|
3
|
+
/** Opaque identity of a specific observer recomputation, captured before await. */
|
|
4
|
+
export interface ObserverPass {
|
|
5
|
+
readonly __solidDepsObserverPass: unique symbol;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Metadata about a synchronous dependency graft. */
|
|
9
|
+
export interface AttachReadResult {
|
|
10
|
+
/** Newly attached/validated sources, INCLUDING promotions. */
|
|
11
|
+
readonly added: number;
|
|
12
|
+
/** Sources already attached as current validated dependencies. */
|
|
13
|
+
readonly duplicate: number;
|
|
14
|
+
/** Prior-generation links reused; a subset of added. */
|
|
15
|
+
readonly promoted: number;
|
|
16
|
+
/** Observer already disposed; callback was not invoked. */
|
|
17
|
+
readonly disposed: boolean;
|
|
18
|
+
/** A newer recomputation superseded the pass; callback was not invoked. */
|
|
19
|
+
readonly stale: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add dependencies read by a synchronous callback to an existing tracking observer.
|
|
24
|
+
* Returns `[retValue, result]` (not `[result, retValue]`).
|
|
25
|
+
* If skipped for stale/disposed target, retValue is undefined.
|
|
26
|
+
* EXPERIMENTAL: accesses private Solid 2.0.0-rc.17 dependency graph layouts.
|
|
27
|
+
*/
|
|
28
|
+
export declare function attachRead<T>(
|
|
29
|
+
observerOrPass: Owner | ObserverPass | null,
|
|
30
|
+
read: () => T
|
|
31
|
+
): [retValue: T | undefined, result: AttachReadResult];
|
|
32
|
+
|
|
33
|
+
/** Capture a specific recomputation inside a tracking callback (before await). */
|
|
34
|
+
export declare function captureObserverPass(observer: Owner | null): ObserverPass;
|
|
35
|
+
|
|
36
|
+
/** Return source node objects in an observer's dependency chain. Diagnostic only. */
|
|
37
|
+
export declare function inspectDeps(observer: Owner): unknown[];
|