solid-deps 0.0.3 → 0.0.5
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/README.md +102 -5
- package/package.json +4 -4
- package/src/attach-read.mjs +42 -12
- package/src/discover.mjs +122 -4
- package/src/index.mjs +1 -1
- package/types/index.d.mts +7 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Or with Bun:
|
|
|
16
16
|
bun add solid-deps solid-js@2.0.0-rc.17 @solidjs/signals@2.0.0-rc.17
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
**Compatibility:** Tested with Solid 2.0.0-rc.17 production, development and observe builds. `solid-deps`
|
|
19
|
+
**Compatibility:** Tested with Solid 2.0.0-rc.17 production, development and observe builds. `solid-deps` recognizes validated descriptive development fields directly and discovers unknown or minified graph fields at runtime. Other Solid 2 versions may work if they retain compatible internal graph semantics, but are not yet verified. Your app and `solid-deps` must share one `@solidjs/signals` runtime instance. SSR is not supported.
|
|
20
20
|
|
|
21
21
|
## Quick start: `createScope()`
|
|
22
22
|
|
|
@@ -67,6 +67,86 @@ const summary = scope(() => ({ count: count(), enabled: enabled() }));
|
|
|
67
67
|
|
|
68
68
|
The callback must be synchronous. Await asynchronous work **before** calling `scope()`, not inside it.
|
|
69
69
|
|
|
70
|
+
## Pending async memos and `NotReadyError`
|
|
71
|
+
|
|
72
|
+
A synchronous callback may read an **existing async memo that is not ready yet**. In that case, Solid 2 throws `NotReadyError` as part of its pending-value protocol. `solid-deps` preserves the dependencies read up to that point **before rethrowing** the same error:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createMemo, createEffect, NotReadyError } from 'solid-js';
|
|
76
|
+
import { createOwnerScope } from 'solid-deps';
|
|
77
|
+
|
|
78
|
+
const memo = createMemo(async () => {
|
|
79
|
+
return await loadData();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
createEffect(() => {
|
|
83
|
+
const scope = createOwnerScope();
|
|
84
|
+
|
|
85
|
+
// Example: an externally scheduled callback after the effect starts.
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
try {
|
|
88
|
+
console.log(scope(() => memo()));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof NotReadyError) {
|
|
91
|
+
// memo() has been subscribed to the original effect.
|
|
92
|
+
// Its successful settlement can invalidate that effect.
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return 0;
|
|
100
|
+
}, () => {});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**What this means:** the pending memo becomes a direct dependency of the original computation, even when `scope()` cannot return a value. If the memo later fulfills, Solid can schedule the original effect; its next normal recomputation may remove the late subscription if the effect no longer reads it. Only sources actually read **before** `NotReadyError` are attached. Other thrown errors still leave the original dependency graph unchanged.
|
|
104
|
+
|
|
105
|
+
`NotReadyError` is **re-thrown**, not converted to `undefined` or an `AttachReadResult`. `createScope()` and `createOwnerScope()` share this behavior. When using a delayed callback outside Solid's tracked execution, catch or handle the error; simply propagating it does not automatically restore the original `<Loading>` boundary context. Fulfillment-based wakeup and basic suspended-effect behavior have been tested on rc.17; complex Loading/transition semantics and error-state recovery are not guaranteed.
|
|
106
|
+
|
|
107
|
+
## Alternative: `createOwnerScope()`
|
|
108
|
+
|
|
109
|
+
`createScope()` is **cycle-bound**: when the effect recomputes, an earlier scope becomes stale. Use `createOwnerScope()` when an older asynchronous callback deliberately needs to attach reads to the **same still-live effect**, even after that effect has rerun.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { createEffect, createSignal } from 'solid-js';
|
|
113
|
+
import { createOwnerScope, DisposedScopeError } from 'solid-deps';
|
|
114
|
+
|
|
115
|
+
const [key, setKey] = createSignal(0);
|
|
116
|
+
const [late, setLate] = createSignal(0);
|
|
117
|
+
|
|
118
|
+
createEffect(() => {
|
|
119
|
+
const ownerScope = createOwnerScope();
|
|
120
|
+
const current = key(); // Native dependency
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
try {
|
|
124
|
+
console.log(ownerScope(() => late()));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!(error instanceof DisposedScopeError)) throw error;
|
|
127
|
+
}
|
|
128
|
+
}, 2000);
|
|
129
|
+
|
|
130
|
+
return current;
|
|
131
|
+
}, () => {});
|
|
132
|
+
|
|
133
|
+
setKey(1); // The old ownerScope still targets the same live computation.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This example deliberately leaves the timeout running to demonstrate owner-bound behavior. **For async work that belongs only to a particular effect execution, prefer `createScope()` and cancel obsolete work using the effect's cleanup mechanism.**
|
|
137
|
+
|
|
138
|
+
Comparison:
|
|
139
|
+
|
|
140
|
+
| | `createScope()` | `createOwnerScope()` |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| Captures | Computation and dependency generation | Computation identity only |
|
|
143
|
+
| Reusable after a normal effect rerun | No — throws `StaleScopeError` | Yes, while the effect is alive |
|
|
144
|
+
| After final disposal | Throws `DisposedScopeError` | Throws `DisposedScopeError` |
|
|
145
|
+
| Callback return | Direct value | Direct value |
|
|
146
|
+
| Extra dependencies survive every rerun | No | No |
|
|
147
|
+
|
|
148
|
+
Owner-bound callbacks do **not** restore Solid ownership context, transfer to a replacement effect, or make attached dependencies permanent. An older callback may alter the currently running effect's dependency graph; use this behavior intentionally.
|
|
149
|
+
|
|
70
150
|
## API
|
|
71
151
|
|
|
72
152
|
### `createScope(owner?)`
|
|
@@ -89,6 +169,21 @@ A scope call:
|
|
|
89
169
|
- Throws `DisposedScopeError` if the computation has been disposed. The callback is not executed.
|
|
90
170
|
- Propagates errors thrown by the callback or the graph-validation mechanism.
|
|
91
171
|
|
|
172
|
+
### `createOwnerScope(owner?)`
|
|
173
|
+
|
|
174
|
+
Create a callable function bound to the lifetime of the current tracked computation, rather than its current execution cycle.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const scope = createOwnerScope(); // While an observer is tracking
|
|
178
|
+
const other = createOwnerScope(owner); // Explicit existing computation owner
|
|
179
|
+
await loadData();
|
|
180
|
+
const value = scope(() => signal()); // Exact callback value
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The implicit form requires an active observer at creation time. An explicit owner must be a tracking computation rather than a plain Solid owner/root. The handle keeps a reference to that **same** computation across normal reruns; it does not discover or switch to any newly created effect. Final disposal causes `DisposedScopeError`, and the callback is skipped. Other reader/graph errors propagate normally.
|
|
184
|
+
|
|
185
|
+
Do not call it to perform tracked reads *inside an effect cleanup callback*: the temporary tracking computation required for grafting can be rejected by Solid in development mode. Use cleanup to cancel external timers or requests instead.
|
|
186
|
+
|
|
92
187
|
### `captureOwnerCycle(owner?)`
|
|
93
188
|
|
|
94
189
|
Capture an opaque owner-cycle token with the same implicit-or-explicit owner behavior as `createScope`:
|
|
@@ -165,6 +260,7 @@ export interface AttachReadResult<T> {
|
|
|
165
260
|
}
|
|
166
261
|
|
|
167
262
|
export declare function createScope(owner?: Owner | null): Scope;
|
|
263
|
+
export declare function createOwnerScope(owner?: Owner | null): Scope;
|
|
168
264
|
export declare function captureOwnerCycle(owner?: Owner | null): OwnerCycle;
|
|
169
265
|
export declare function attachRead<T>(
|
|
170
266
|
target: Owner | OwnerCycle | null,
|
|
@@ -181,18 +277,19 @@ With `createScope()`, a callback returning `number` gives a `number` directly. W
|
|
|
181
277
|
|
|
182
278
|
- An attached read affects **future** invalidations. Writes that occurred before registration are not replayed.
|
|
183
279
|
- Extra dependencies can be removed on the target's next normal recomputation. Reattach them on each async effect execution when needed.
|
|
184
|
-
- `scope()` / `attachRead()` callbacks must
|
|
280
|
+
- `scope()` / `attachRead()` callbacks must be synchronous. They may read existing pending memos: `NotReadyError` is propagated **after** transferring previously read dependencies. Ordinary thrown errors leave the target unchanged. Creating owned computations, registering cleanups, or mutating signals from inside callbacks is not supported.
|
|
185
281
|
- A callback that allocates owned children/cleanups, or reads a newly allocated probe-owned memo, is rejected. Access existing independently owned memos instead.
|
|
186
282
|
- Reads happen under a temporary observer; `getOwner()`, `getObserver()`, pending resource behavior, transitions, Loading boundaries, and ownership-sensitive operations are **not equivalent** to running under the original effect.
|
|
187
|
-
-
|
|
188
|
-
-
|
|
283
|
+
- A pending memo can throw `NotReadyError`, and the attached observer will keep the dependency for successful settlement. This does not recreate the original Loading boundary or guarantee retry after rejection. Advanced suspense/transition and held-transaction behavior is not broadly supported.
|
|
284
|
+
- Cycle-bound `createScope()` rejects an older execution after rerun; `createOwnerScope()` stays valid for the same live observer, but old callbacks can change the new execution's dependencies. Neither automatically cancels work.
|
|
285
|
+
- Do not graft reads from inside effect-phase cleanup; use cleanup to cancel obsolete external work. Final disposal invalidates both scope types.
|
|
189
286
|
- Uses private observer graph internals discovered by inspecting disposable runtime probes. This supports field renaming when graph relationships remain compatible, but cannot guarantee future graph algorithms, scheduler semantics, or minifier property restructuring. Discovery fails with an error if required fields are ambiguous or missing.
|
|
190
287
|
- Calibrates only once per loaded package instance. In an ambiguous numeric-generation case, a field-position clue is used after checking link-generation equality; reordered internal fields may therefore cause calibration to fail or be misidentified. Test on new versions before shipping.
|
|
191
288
|
- Other Solid versions, SSR, and multiple separately instantiated signal runtimes are not guaranteed.
|
|
192
289
|
|
|
193
290
|
## Online Solid playground check
|
|
194
291
|
|
|
195
|
-
A copy-pasteable `main.tsx` is available in the source ZIP at `examples/online-playground/main.tsx`. Install `solid-deps@0.0.
|
|
292
|
+
A copy-pasteable `main.tsx` is available in the source ZIP at `examples/online-playground/main.tsx`. Install `solid-deps@0.0.5`, ensure the playground runs a Solid 2 browser build with a shared signals runtime, and use **Run runtime checks**. It reports successful automatic layout discovery, async dependency attachment, deduplication, future updates, stale cycles, and disposal.
|
|
196
293
|
|
|
197
294
|
## License
|
|
198
295
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-deps",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Experimental direct dependency attachment for Solid 2 reactive observers after async boundaries",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
39
|
"setup": "node scripts/setup.mjs",
|
|
40
|
-
"test": "node --conditions=browser --test test/test.mjs test/test-async.mjs test/test-return.mjs test/test-scope.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 test/test-discovery.mjs",
|
|
41
|
-
"test:dev": "node --conditions=browser --conditions=development --test test/test.mjs test/test-async.mjs test/test-return.mjs test/test-scope.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 test/test-discovery.mjs",
|
|
42
|
-
"test:observe": "node --conditions=browser --conditions=observe --test test/test.mjs test/test-async.mjs test/test-return.mjs test/test-scope.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 test/test-discovery.mjs",
|
|
40
|
+
"test": "node --conditions=browser --test test/test.mjs test/test-async.mjs test/test-return.mjs test/test-scope.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 test/test-discovery.mjs test/test-fastpath.mjs test/test-owner-bound-research.mjs test/test-owner-scope.mjs test/test-notready-lifecycle.mjs",
|
|
41
|
+
"test:dev": "node --conditions=browser --conditions=development --test test/test.mjs test/test-async.mjs test/test-return.mjs test/test-scope.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 test/test-discovery.mjs test/test-fastpath.mjs test/test-owner-bound-research.mjs test/test-owner-scope.mjs test/test-notready-lifecycle.mjs",
|
|
42
|
+
"test:observe": "node --conditions=browser --conditions=observe --test test/test.mjs test/test-async.mjs test/test-return.mjs test/test-scope.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 test/test-discovery.mjs test/test-fastpath.mjs test/test-owner-bound-research.mjs test/test-owner-scope.mjs test/test-notready-lifecycle.mjs",
|
|
43
43
|
"test:all": "node scripts/run-tests.mjs",
|
|
44
44
|
"test:package": "node scripts/smoke-package.mjs",
|
|
45
45
|
"test:browser": "node --test test/browser/playwright.test.mjs",
|
package/src/attach-read.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Experimental RC.17 graph surgery. No changes/forks of Solid.
|
|
3
3
|
* Target must be captured using getObserver() while its tracking function runs.
|
|
4
|
-
* Scope: synchronous,
|
|
4
|
+
* Scope: synchronous reactive reads, including pending memo reads that throw NotReadyError.
|
|
5
|
+
* Reads execute in a temporary observer; pending dependencies can be transferred,
|
|
6
|
+
* but this does not restore the target effect's Suspense/Loading context.
|
|
5
7
|
* Maintenance/compatibility: see HANDOFF.md and docs/INTERNALS.md.
|
|
6
8
|
* This is link transfer, NOT restoration of the original observer context.
|
|
7
9
|
*/
|
|
8
|
-
import { createRoot, createReaction, getObserver, isDisposed } from '@solidjs/signals';
|
|
10
|
+
import { createRoot, createReaction, getObserver, isDisposed, NotReadyError } from '@solidjs/signals';
|
|
9
11
|
|
|
10
12
|
import { layoutOf } from './discover.mjs';
|
|
11
13
|
|
|
@@ -45,7 +47,9 @@ export function captureOwnerCycle(owner = getObserver()) {
|
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Attach dependencies read by `read` onto `target` without re-running target.
|
|
48
|
-
* Returns an object { value, added, duplicate, promoted, stale, disposed }
|
|
50
|
+
* Returns an object { value, added, duplicate, promoted, stale, disposed }
|
|
51
|
+
* on successful reads. A NotReadyError preserves tracked dependencies before
|
|
52
|
+
* rethrowing so pending memos can wake the target; other exceptions roll back.
|
|
49
53
|
* They remain until target recomputes and performs normal dependency reconciliation.
|
|
50
54
|
*/
|
|
51
55
|
export function attachRead(observerOrCycle, read) {
|
|
@@ -80,7 +84,13 @@ export function attachRead(observerOrCycle, read) {
|
|
|
80
84
|
try { retValue = read(); } catch (err) { caught = [err]; }
|
|
81
85
|
});
|
|
82
86
|
});
|
|
83
|
-
|
|
87
|
+
// Solid's normal tracking pass keeps dependencies read before NotReadyError,
|
|
88
|
+
// so pending sources can wake the computation after settling. Preserve the
|
|
89
|
+
// same dependency semantics for a late read, while still propagating the
|
|
90
|
+
// suspension signal to the caller. Other exceptions remain transactional:
|
|
91
|
+
// they must not mutate the target observer's graph.
|
|
92
|
+
const pendingError = caught?.[0] instanceof NotReadyError ? caught[0] : null;
|
|
93
|
+
if (caught && !pendingError) throw caught[0];
|
|
84
94
|
if (retValue != null && typeof retValue.then === 'function')
|
|
85
95
|
throw new TypeError('attachRead requires synchronous reads, not an async callback');
|
|
86
96
|
if (!probe) throw new Error('No temporary observer was captured');
|
|
@@ -150,6 +160,9 @@ export function attachRead(observerOrCycle, read) {
|
|
|
150
160
|
nextList[i][s.next] = nextList[i + 1] ?? null;
|
|
151
161
|
target[s.deps] = nextList[0] ?? null;
|
|
152
162
|
target[s.tail] = validated.at(-1) ?? null;
|
|
163
|
+
// Re-throw only AFTER the graph transfer has succeeded. The temporary
|
|
164
|
+
// probe is disposed in finally; its moved edges now belong to the target.
|
|
165
|
+
if (pendingError) throw pendingError;
|
|
153
166
|
return { value: retValue,
|
|
154
167
|
added: additions.length,
|
|
155
168
|
duplicate: kept.length - promoted.length,
|
|
@@ -182,17 +195,34 @@ export class DisposedScopeError extends Error {
|
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
187
|
-
* The callback must be synchronous. Its return value is returned directly.
|
|
188
|
-
* Throws instead of returning an ambiguous undefined when the cycle has ended.
|
|
189
|
-
*/
|
|
190
|
-
export function createScope(owner) {
|
|
191
|
-
const cycle = arguments.length === 0 ? captureOwnerCycle() : captureOwnerCycle(owner);
|
|
198
|
+
/** Shared callable facade for owner-bound and cycle-bound dependency scopes. */
|
|
199
|
+
function makeScope(target) {
|
|
192
200
|
return function scope(read) {
|
|
193
|
-
const result = attachRead(
|
|
201
|
+
const result = attachRead(target, read);
|
|
194
202
|
if (result.disposed) throw new DisposedScopeError();
|
|
195
203
|
if (result.stale) throw new StaleScopeError();
|
|
196
204
|
return result.value;
|
|
197
205
|
};
|
|
198
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Capture the current observer cycle and return a callable dependency scope.
|
|
210
|
+
* The callback must be synchronous; it may throw NotReadyError after grafting
|
|
211
|
+
* its pending dependencies. An earlier cycle becomes stale after rerun.
|
|
212
|
+
*/
|
|
213
|
+
export function createScope(owner) {
|
|
214
|
+
return makeScope(arguments.length === 0 ? captureOwnerCycle() : captureOwnerCycle(owner));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Capture a live computation owner, without binding to its dependency generation.
|
|
219
|
+
* The returned callback remains usable across that computation's normal reruns,
|
|
220
|
+
* but rejects reads after its final disposal. An old callback can therefore
|
|
221
|
+
* attach dependencies to the newest execution of the same computation.
|
|
222
|
+
*/
|
|
223
|
+
export function createOwnerScope(owner) {
|
|
224
|
+
const observer = arguments.length === 0 ? getObserver() : owner;
|
|
225
|
+
// Validate at capture time, including the initial empty dependency list.
|
|
226
|
+
layoutOf(observer);
|
|
227
|
+
return makeScope(observer);
|
|
228
|
+
}
|
package/src/discover.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Discover the private linked dependency graph from controlled, disposable
|
|
3
|
-
* Solid computations.
|
|
3
|
+
* Solid computations. Only recognizable descriptive development property names
|
|
4
|
+
* are encoded; all unknown layouts use the name-independent inference.
|
|
4
5
|
*
|
|
5
6
|
* This is topology inference, not a guarantee of compatibility with a new
|
|
6
7
|
* algorithm or a property-mangling transform that changes graph semantics.
|
|
@@ -11,6 +12,111 @@ import {
|
|
|
11
12
|
} from '@solidjs/signals';
|
|
12
13
|
|
|
13
14
|
let inferred;
|
|
15
|
+
const diagnostics = { knownDev: 0, inferred: 0 };
|
|
16
|
+
|
|
17
|
+
/** Internal test-only counters. Not exported from the package entry point. */
|
|
18
|
+
export function _discoveryDiagnostics() {
|
|
19
|
+
return { ...diagnostics, origin: inferred?.origin ?? null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Development field names are a candidate, never evidence of graph compatibility.
|
|
23
|
+
// Minified field names are deliberately not recorded here.
|
|
24
|
+
const knownDev = Object.freeze({
|
|
25
|
+
deps: '_deps', tail: '_depsTail', gen: '_depGen', flags: '_flags',
|
|
26
|
+
dep: '_dep', sub: '_sub', next: '_nextDep', linkGen: '_gen',
|
|
27
|
+
parent: '_parent', child: '_firstChild', disposal: '_disposal',
|
|
28
|
+
recomputingBit: 4
|
|
29
|
+
});
|
|
30
|
+
const devFields = ['deps', 'tail', 'gen', 'flags', 'parent', 'child', 'disposal'];
|
|
31
|
+
|
|
32
|
+
// Only inspect own *data* properties to avoid executing an arbitrary getter
|
|
33
|
+
// during compatibility detection. A missing data field is not silently read.
|
|
34
|
+
function data(node, key) {
|
|
35
|
+
if (!obj(node)) return { exists: false };
|
|
36
|
+
const descriptor = Object.getOwnPropertyDescriptor(node, key);
|
|
37
|
+
return descriptor && 'value' in descriptor
|
|
38
|
+
? { exists: true, value: descriptor.value }
|
|
39
|
+
: { exists: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure, non-mutating candidate check; the initial observer may have zero deps,
|
|
44
|
+
* or a validated prefix followed by an older-generation stale suffix.
|
|
45
|
+
* `null` means unknown or partially renamed; corrupted complete layouts throw.
|
|
46
|
+
* Kept as an internal module export for the source-tree regression suite.
|
|
47
|
+
*/
|
|
48
|
+
export function tryKnownDevLayout(node) {
|
|
49
|
+
if (!obj(node)) return null;
|
|
50
|
+
const present = devFields.filter(role => Object.hasOwn(node, knownDev[role]));
|
|
51
|
+
if (present.length !== devFields.length) return null;
|
|
52
|
+
|
|
53
|
+
const read = (object, key, role) => {
|
|
54
|
+
const slot = data(object, key);
|
|
55
|
+
if (!slot.exists) throw new Error(`solid-deps: invalid known development ${role}; refusing graph mutation`);
|
|
56
|
+
return slot.value;
|
|
57
|
+
};
|
|
58
|
+
const fail = why => { throw new Error(`solid-deps: invalid known development graph (${why}); refusing graph mutation`); };
|
|
59
|
+
const gen = read(node, knownDev.gen, 'generation');
|
|
60
|
+
const flags = read(node, knownDev.flags, 'flags');
|
|
61
|
+
const head = read(node, knownDev.deps, 'head');
|
|
62
|
+
const tail = read(node, knownDev.tail, 'tail');
|
|
63
|
+
const parent = read(node, knownDev.parent, 'parent');
|
|
64
|
+
const child = read(node, knownDev.child, 'child');
|
|
65
|
+
const disposal = read(node, knownDev.disposal, 'disposal');
|
|
66
|
+
|
|
67
|
+
if (!Number.isSafeInteger(gen) || gen < 0 || !Number.isSafeInteger(flags) || flags < 0)
|
|
68
|
+
fail('generation or flags is not a nonnegative integer');
|
|
69
|
+
// Multiple onCleanup() calls create an array of disposal callbacks.
|
|
70
|
+
// Check their data slots without invoking a potentially shadowed .every().
|
|
71
|
+
let validDisposal = disposal == null || typeof disposal === 'function';
|
|
72
|
+
if (Array.isArray(disposal)) {
|
|
73
|
+
validDisposal = disposal.length > 0;
|
|
74
|
+
for (let i = 0; i < disposal.length; i++) {
|
|
75
|
+
const callback = data(disposal, String(i));
|
|
76
|
+
if (!callback.exists || typeof callback.value !== 'function') validDisposal = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (head != null && !obj(head) || tail != null && !obj(tail) ||
|
|
80
|
+
parent != null && !obj(parent) || child != null && !obj(child) ||
|
|
81
|
+
!validDisposal)
|
|
82
|
+
fail('invalid dependency or lifecycle value');
|
|
83
|
+
if (head == null && tail != null) fail('tail exists without a head');
|
|
84
|
+
|
|
85
|
+
let edge = head;
|
|
86
|
+
let tailReached = tail == null;
|
|
87
|
+
const visited = new Set();
|
|
88
|
+
while (edge != null) {
|
|
89
|
+
if (!obj(edge) || visited.has(edge)) fail('non-link or cyclic dependency chain');
|
|
90
|
+
visited.add(edge);
|
|
91
|
+
const source = read(edge, knownDev.dep, 'link source');
|
|
92
|
+
const subscriber = read(edge, knownDev.sub, 'link subscriber');
|
|
93
|
+
const linkGen = read(edge, knownDev.linkGen, 'link generation');
|
|
94
|
+
const next = read(edge, knownDev.next, 'next link');
|
|
95
|
+
if (!obj(source) || subscriber !== node || !Number.isSafeInteger(linkGen) ||
|
|
96
|
+
linkGen < 0 || linkGen > gen || next != null && !obj(next))
|
|
97
|
+
fail('inconsistent dependency link');
|
|
98
|
+
|
|
99
|
+
// Check both sides of the source subscription list. In rc.17 dev builds,
|
|
100
|
+
// source._subs and edge._nextSub connect each signal back to its readers.
|
|
101
|
+
// This is a read-only check, never an attempt to repair graph structure.
|
|
102
|
+
let sub = read(source, '_subs', 'source subscriber head');
|
|
103
|
+
const subscribers = new Set();
|
|
104
|
+
let found = false;
|
|
105
|
+
while (sub != null) {
|
|
106
|
+
if (!obj(sub) || subscribers.has(sub)) fail('invalid source subscriber list');
|
|
107
|
+
subscribers.add(sub);
|
|
108
|
+
if (sub === edge) found = true;
|
|
109
|
+
sub = read(sub, '_nextSub', 'subscriber next');
|
|
110
|
+
}
|
|
111
|
+
if (!found) fail('source is not subscribed back to dependency link');
|
|
112
|
+
|
|
113
|
+
if (edge === tail) tailReached = true;
|
|
114
|
+
edge = next;
|
|
115
|
+
}
|
|
116
|
+
if (!tailReached) fail('tail not reachable from dependency head');
|
|
117
|
+
return knownDev;
|
|
118
|
+
}
|
|
119
|
+
|
|
14
120
|
const own = (obj) => obj && typeof obj === 'object'
|
|
15
121
|
? Reflect.ownKeys(obj).filter(key => Object.prototype.propertyIsEnumerable.call(obj, key)) : [];
|
|
16
122
|
const entries = obj => own(obj).map(key => [key, obj[key]]);
|
|
@@ -157,12 +263,24 @@ function discover() {
|
|
|
157
263
|
}
|
|
158
264
|
}
|
|
159
265
|
|
|
160
|
-
export function internalLayout() {
|
|
161
|
-
|
|
266
|
+
export function internalLayout(node) {
|
|
267
|
+
if (inferred) return inferred.layout;
|
|
268
|
+
// Called with the real target by layoutOf; keep the optional no-argument
|
|
269
|
+
// form for internal source-tree tests and unknown-layout calibration.
|
|
270
|
+
const dev = node == null ? null : tryKnownDevLayout(node);
|
|
271
|
+
if (dev) {
|
|
272
|
+
diagnostics.knownDev++;
|
|
273
|
+
inferred = { layout: dev, origin: 'known-dev' };
|
|
274
|
+
} else {
|
|
275
|
+
const layout = discover();
|
|
276
|
+
diagnostics.inferred++;
|
|
277
|
+
inferred = { layout, origin: 'inferred' };
|
|
278
|
+
}
|
|
279
|
+
return inferred.layout;
|
|
162
280
|
}
|
|
163
281
|
|
|
164
282
|
export function layoutOf(node) {
|
|
165
|
-
const shape = internalLayout();
|
|
283
|
+
const shape = internalLayout(node);
|
|
166
284
|
if (!obj(node) || typeof node[shape.gen] !== 'number' ||
|
|
167
285
|
!(shape.deps in node) || !(shape.tail in node) ||
|
|
168
286
|
typeof node[shape.flags] !== 'number' || !(shape.child in node) ||
|
package/src/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { attachRead, captureOwnerCycle, createScope, inspectDeps, StaleScopeError, DisposedScopeError } from './attach-read.mjs';
|
|
1
|
+
export { attachRead, captureOwnerCycle, createScope, createOwnerScope, inspectDeps, StaleScopeError, DisposedScopeError } from './attach-read.mjs';
|
package/types/index.d.mts
CHANGED
|
@@ -47,6 +47,13 @@ export declare function captureOwnerCycle(owner?: Owner | null): OwnerCycle;
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function createScope(owner?: Owner | null): Scope;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Create an owner-bound callable scope. Unlike createScope(), it stays valid
|
|
52
|
+
* across normal reruns of the same computation and rejects final disposal.
|
|
53
|
+
* Captured work from an older execution may attach to the current execution.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createOwnerScope(owner?: Owner | null): Scope;
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* Graft dependencies read synchronously by `read` onto an existing observer.
|
|
52
59
|
* The callback is skipped when the target is stale or disposed.
|