solid-deps 0.0.5 → 0.0.8
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 +358 -16
- package/package.json +32 -7
- package/src/attach-read.mjs +126 -0
- package/src/delegated-scope.mjs +152 -0
- package/src/hybrid-scope.mjs +47 -0
- package/src/index.mjs +3 -1
- package/src/retained-scope.mjs +64 -0
- package/types/delegated.d.mts +63 -0
- package/types/index.d.mts +51 -2
package/README.md
CHANGED
|
@@ -1,24 +1,55 @@
|
|
|
1
1
|
# solid-deps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Restore Solid 2's reactive **tracking or owner context** for synchronous callbacks after an `await`.
|
|
4
4
|
|
|
5
|
-
`
|
|
5
|
+
By default, `createScope()` chooses **one of two behaviors at capture time**:
|
|
6
|
+
|
|
7
|
+
- **Tracking observer present (effect or memo):** capture the observer cycle and attach late reactive reads to it. Stale and disposed scopes reject calls.
|
|
8
|
+
- **Plain owner present (component/owner body without tracking):** restore that owner using `runWithOwner()` for synchronous callbacks. This restores context and owned cleanups/children **but does not subscribe signals**.
|
|
9
|
+
|
|
10
|
+
For a strictly observer-only factory, use `createObserverScope()`. The explicit owner argument to `createScope(owner)` continues to require a tracking Computed for backwards compatibility. `createOwnerScope()` is unchanged.
|
|
6
11
|
|
|
7
12
|
## Install
|
|
8
13
|
|
|
9
14
|
```sh
|
|
10
|
-
npm install solid-deps solid-js@2.0.0-rc.
|
|
15
|
+
npm install solid-deps solid-js@2.0.0-rc.18 @solidjs/signals@2.0.0-rc.18
|
|
11
16
|
```
|
|
12
17
|
|
|
13
18
|
Or with Bun:
|
|
14
19
|
|
|
15
20
|
```sh
|
|
16
|
-
bun add solid-deps solid-js@2.0.0-rc.
|
|
21
|
+
bun add solid-deps solid-js@2.0.0-rc.18 @solidjs/signals@2.0.0-rc.18
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Compatibility:** This hybrid feature is tested with Solid 2.0.0-rc.17 **and** 2.0.0-rc.18; rc.20 is pending verification. The preexisting package was tested with Solid 2.0.0-rc.17 **and** 2.0.0-rc.18 production, development and observe builds (Node/ESM regression and package consumer tests, plus real Chromium owner-bound bridge scenarios). `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.
|
|
25
|
+
|
|
26
|
+
## Quick start: hybrid `createScope()`
|
|
27
|
+
|
|
28
|
+
**Component body (plain-owner fallback):**
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { getOwner, onCleanup } from 'solid-js';
|
|
32
|
+
import { createScope } from 'solid-deps';
|
|
33
|
+
|
|
34
|
+
function Component() {
|
|
35
|
+
const scope = createScope(); // No tracking observer; captures component owner
|
|
36
|
+
|
|
37
|
+
async function load() {
|
|
38
|
+
await fetch('/api/items');
|
|
39
|
+
scope(() => {
|
|
40
|
+
console.log(getOwner() !== null); // restored owner
|
|
41
|
+
onCleanup(() => console.log('component disposed'));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void load();
|
|
46
|
+
return <div>Loaded asynchronously</div>;
|
|
47
|
+
}
|
|
17
48
|
```
|
|
18
49
|
|
|
19
|
-
|
|
50
|
+
Read accessors under this plain-owner fallback **do not** create subscriptions in the component. `runWithOwner()` deliberately clears tracking. Children and cleanups created inside the callback belong to the captured owner and are disposed with it. The callback itself must be synchronous; if it contains `await`, context is **not** restored after the `await`. Promise-returning callbacks are rejected.
|
|
20
51
|
|
|
21
|
-
|
|
52
|
+
**Tracked effect or memo:**
|
|
22
53
|
|
|
23
54
|
Capture a dependency scope inside the tracked part of your computation. Call it after `await` to attach signal reads to that same computation:
|
|
24
55
|
|
|
@@ -52,7 +83,9 @@ export default function Counter() {
|
|
|
52
83
|
}
|
|
53
84
|
```
|
|
54
85
|
|
|
55
|
-
`scope(callback)` is a **callable function** whose result is
|
|
86
|
+
`scope(callback)` is a **callable function** whose result is the callback's return value. **Only in the tracked-observer branch**, it grafts reactive sources read synchronously by the callback into the captured observer. It does **not** restore that observer's original owner context: reads run under a temporary tracking observer and their subscriptions are transferred.
|
|
87
|
+
|
|
88
|
+
In the plain-owner branch, it uses `runWithOwner()` and does **not** graft dependencies. The branch is fixed when `createScope()` is called, even if the wrapper is invoked from a different context later.
|
|
56
89
|
|
|
57
90
|
A scope can be called multiple times while its captured observer cycle is still current:
|
|
58
91
|
|
|
@@ -102,11 +135,13 @@ createEffect(() => {
|
|
|
102
135
|
|
|
103
136
|
**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
137
|
|
|
105
|
-
|
|
138
|
+
A later pending transition behaves differently from an initial pending read. If an already-settled memo is attached and **then** starts another async request, Solid can propagate its pending status through the existing link, delaying the target effect’s apply phase until settlement. This does **not** make late reads persistent: if the target recomputes without reading the memo again, Solid may remove its subscription. With multiple pending memos, the target can therefore apply after one settles while another remains pending. A native effect that reads both memos in its own compute function can keep waiting. `solid-deps` does not replay the scoped callback or automatically repeat its pending reads on subsequent effect passes.
|
|
139
|
+
|
|
140
|
+
`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 and rc.18; complex Loading/transition semantics and error-state recovery are not guaranteed.
|
|
106
141
|
|
|
107
142
|
## Alternative: `createOwnerScope()`
|
|
108
143
|
|
|
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.
|
|
144
|
+
`createScope()` is **cycle-bound when an observer is captured**: 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
145
|
|
|
111
146
|
```tsx
|
|
112
147
|
import { createEffect, createSignal } from 'solid-js';
|
|
@@ -137,7 +172,7 @@ This example deliberately leaves the timeout running to demonstrate owner-bound
|
|
|
137
172
|
|
|
138
173
|
Comparison:
|
|
139
174
|
|
|
140
|
-
| | `createScope()` | `createOwnerScope()` |
|
|
175
|
+
| | `createScope()` (observer branch) | `createOwnerScope()` |
|
|
141
176
|
| --- | --- | --- |
|
|
142
177
|
| Captures | Computation and dependency generation | Computation identity only |
|
|
143
178
|
| Reusable after a normal effect rerun | No — throws `StaleScopeError` | Yes, while the effect is alive |
|
|
@@ -149,9 +184,15 @@ Owner-bound callbacks do **not** restore Solid ownership context, transfer to a
|
|
|
149
184
|
|
|
150
185
|
## API
|
|
151
186
|
|
|
152
|
-
### `createScope(owner?)`
|
|
187
|
+
### `createScope(owner?)` / `createHybridScope(owner?)`
|
|
188
|
+
|
|
189
|
+
The default and explicit hybrid alias are the same function. Without arguments, capture the current tracking observer if present; otherwise capture the current plain owner. If neither exists, throw immediately. With an **explicit owner** argument, preserve the preexisting strict observer-only behavior: the supplied owner must be a tracking Computed.
|
|
153
190
|
|
|
154
|
-
|
|
191
|
+
When an observer is captured, calls attach reads to its captured dependency generation. When only a plain owner is captured, calls restore owner/context synchronously, without tracking reactive dependencies. A disposed owner throws `DisposedScopeError` before the callback executes.
|
|
192
|
+
|
|
193
|
+
### `createObserverScope(owner?)`
|
|
194
|
+
|
|
195
|
+
The strict observer-only equivalent of the pre-hybrid `createScope()`. Use it when a missing tracking computation must be an error even inside a component owner. It has the same stale-cycle and disposed behavior as the original graph attachment factory.
|
|
155
196
|
|
|
156
197
|
```ts
|
|
157
198
|
const scope = createScope(); // Capture current tracked observer implicitly
|
|
@@ -160,7 +201,7 @@ const scope2 = createScope(owner); // Explicit existing computation owner
|
|
|
160
201
|
const value = scope(() => signal());
|
|
161
202
|
```
|
|
162
203
|
|
|
163
|
-
The implicit form must be invoked
|
|
204
|
+
The implicit form must be invoked **before `await`**, while the intended observer or owner is active. The explicit owner parameter remains strictly observer-only; it must be a tracking computation, not a plain root/component owner.
|
|
164
205
|
|
|
165
206
|
A scope call:
|
|
166
207
|
|
|
@@ -226,6 +267,30 @@ console.log(result.added, result.duplicate, result.promoted);
|
|
|
226
267
|
|
|
227
268
|
`attachRead()` returns status rather than throwing for stale/disposed targets. Reader exceptions are propagated; a Promise/thenable callback result is rejected.
|
|
228
269
|
|
|
270
|
+
### `attachReads(target, callbacks)` — batched dependency attachment (experimental)
|
|
271
|
+
|
|
272
|
+
Use this when **several independent synchronous callbacks** need to attach reactive reads to the same existing observer. A single `attachRead()` callback may already read many signals; this API avoids repeatedly setting up a temporary observer and repeatedly reconciling the target's entire dependency list.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { attachReads } from 'solid-deps';
|
|
276
|
+
|
|
277
|
+
const result = attachReads(owner, [
|
|
278
|
+
() => firstName(),
|
|
279
|
+
() => lastName(),
|
|
280
|
+
() => profile().age
|
|
281
|
+
]);
|
|
282
|
+
// result.values contains [firstName, lastName, age] in callback order.
|
|
283
|
+
// result.added / duplicate / promoted report reactive sources, not callback count.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Each callback can itself read **multiple** signals. All encountered dependencies, including nested direct signal reads, are collected through one temporary tracking computation; reading an existing memo ordinarily attaches the memo source, not all of its own transitive dependencies.
|
|
287
|
+
|
|
288
|
+
If a callback throws `NotReadyError`, subsequent callbacks are **still attempted** so all independently pending sources can be attached. After successful graph transfer, the first `NotReadyError` is rethrown. There is no `values` result in this case. A non-`NotReadyError` exception, an async/thenable callback return, an owned child computation, or an invalid temporary source rejects the batch without grafting its captured links. The target remains subject to the same callback and ownership restrictions as `attachRead()`.
|
|
289
|
+
|
|
290
|
+
On success, `attachReads()` returns `{ values, added, duplicate, promoted, disposed, stale }`. `values` is the ordered array of callback return values, or `undefined` if a stale/disposed target caused the callbacks to be skipped. `target` accepts a live observer or `captureOwnerCycle()` token. An empty array is a no-op.
|
|
291
|
+
|
|
292
|
+
**Important:** batching only improves the cost of *late attachment*. It does not retroactively trigger native `NotReady` status or Loading boundary bookkeeping. A normal computation can still prune grafted links if its own compute function doesn't read those sources. For ongoing native pending behavior use explicit pure retained accessors and normal tracked recomputation where appropriate.
|
|
293
|
+
|
|
229
294
|
### `inspectDeps(owner)`
|
|
230
295
|
|
|
231
296
|
Return the dependency source **node objects** currently linked to the supplied computation, for diagnostics:
|
|
@@ -260,6 +325,8 @@ export interface AttachReadResult<T> {
|
|
|
260
325
|
}
|
|
261
326
|
|
|
262
327
|
export declare function createScope(owner?: Owner | null): Scope;
|
|
328
|
+
export declare function createHybridScope(owner?: Owner | null): Scope;
|
|
329
|
+
export declare function createObserverScope(owner?: Owner | null): Scope;
|
|
263
330
|
export declare function createOwnerScope(owner?: Owner | null): Scope;
|
|
264
331
|
export declare function captureOwnerCycle(owner?: Owner | null): OwnerCycle;
|
|
265
332
|
export declare function attachRead<T>(
|
|
@@ -273,14 +340,248 @@ export declare class DisposedScopeError extends Error {}
|
|
|
273
340
|
|
|
274
341
|
With `createScope()`, a callback returning `number` gives a `number` directly. With `attachRead()`, `result.value` is `number | undefined` because a captured cycle can be stale or disposed.
|
|
275
342
|
|
|
276
|
-
##
|
|
343
|
+
## Delegated scopes: `solid-deps/delegated`
|
|
344
|
+
|
|
345
|
+
Attach new reactive dependencies to an existing Solid 2 effect or memo, including after an asynchronous boundary, **without attaching those dependencies directly to its observer graph**.
|
|
346
|
+
|
|
347
|
+
The delegated scope tracks its readers externally. The original effect or memo subscribes to **one pending-aware reactive bridge**. That bridge notifies the parent when registered dependencies change and can hold its next application while an async dependency is not ready.
|
|
348
|
+
|
|
349
|
+
This is an experimental Solid 2 API. It uses Solid's reactive primitives rather than private graph-link manipulation.
|
|
350
|
+
|
|
351
|
+
### Install and import
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
npm install solid-deps
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { createScope } from 'solid-deps/delegated';
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`solid-deps/delegated` exports **two runtime values** (`createScope`, `DisposedScopeError`) and the **`DelegatedScope` TypeScript interface**. It is separate from the graph-based `solid-deps` entry point.
|
|
362
|
+
|
|
363
|
+
### Quick start: add a late dependency
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { createEffect, createSignal } from 'solid-js';
|
|
367
|
+
import { createScope } from 'solid-deps/delegated';
|
|
368
|
+
|
|
369
|
+
const [count, setCount] = createSignal(0);
|
|
370
|
+
const [late, setLate] = createSignal(10);
|
|
371
|
+
|
|
372
|
+
let scope!: ReturnType<typeof createScope>;
|
|
373
|
+
|
|
374
|
+
createEffect(
|
|
375
|
+
() => {
|
|
376
|
+
scope = createScope();
|
|
377
|
+
return count();
|
|
378
|
+
},
|
|
379
|
+
value => console.log('effect applied:', value)
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Later: an event handler, promise continuation, or other callback.
|
|
383
|
+
const current = scope(() => late()); // 10; also attaches late()
|
|
384
|
+
setLate(11); // Notifies the original effect
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`scope(read)` **reads immediately**, returns the accessor's value, and registers its reactive dependencies for future invalidation. It does not change the meaning of the original effect's already completed application.
|
|
388
|
+
|
|
389
|
+
By default, these late registrations are **per-pass**, just like native dependency reconciliation: when the parent recomputes, readers not registered again in that pass are removed. Use `{ retain: true }` for one-time registrations that should continue to participate in subsequent passes.
|
|
390
|
+
|
|
391
|
+
### One reader or several
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
const value = scope(() => signalA());
|
|
395
|
+
const sum = scope(() => signalA() + signalB());
|
|
396
|
+
const values = scope.batch([() => signalA(), () => signalB()]);
|
|
397
|
+
// values is a typed tuple [A, B]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
A single callback may read several dependencies. `batch()` is useful when readers are independent: if A is initially pending, B is still visited and registered. If a pending accessor throws `NotReadyError`, `scope()` or `batch()` can throw **after** capturing the reached subscriptions. Ordinary errors and returned Promises/thenables do not commit a failed registration.
|
|
401
|
+
|
|
402
|
+
Readers must be **pure synchronous callbacks**. Read existing signals or memos; do not create computations, register cleanups, perform writes, or return promises inside readers. Readers may be evaluated again as part of change, pending, and error propagation.
|
|
403
|
+
|
|
404
|
+
### Subscription lifetime
|
|
405
|
+
|
|
406
|
+
#### Default: native-style per-pass pruning
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const scope = createScope(); // same as { retain: false }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
A late registration stays active through the next applicable parent pass; it is removed if that pass does not re-register it. The cached scope function itself remains stable and reusable while its parent observer stays alive. Late work can register again on that latest live owner.
|
|
413
|
+
|
|
414
|
+
#### Opt-in: retain registrations
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
const scope = createScope({ retain: true });
|
|
418
|
+
|
|
419
|
+
// Later, normally only once:
|
|
420
|
+
scope(() => signalA());
|
|
421
|
+
scope(() => signalB());
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Registrations survive subsequent parent recomputations until removed or disposed. **Retention preserves reader callbacks, not every signal those callbacks have ever touched.** For a conditional reader, dependencies are reconciled inside the reader:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
scope.subscribe(() => enabled() ? asyncMemoA() : undefined);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
When `enabled()` becomes false, the retained callback remains registered but no longer needs A. Switching back to true can track A again, including if A is pending.
|
|
431
|
+
|
|
432
|
+
Choose the same options on every `createScope()` call in a given observer. Changing them for a live observer throws.
|
|
433
|
+
|
|
434
|
+
### Remove a specific registration or clear all
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
const removeA = scope.subscribe(() => signalA());
|
|
438
|
+
const removeB = scope.subscribe(() => signalB());
|
|
439
|
+
|
|
440
|
+
removeA(); // Only this registration is removed
|
|
441
|
+
removeA(); // Safe to call more than once
|
|
442
|
+
scope.clear(); // Remove all current registrations
|
|
443
|
+
|
|
444
|
+
// The scope stays reusable:
|
|
445
|
+
scope(() => signalC());
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`subscribe(read)` returns **an unsubscribe function**, not the current read value. It still evaluates and registers the reader. To obtain the value directly, use `scope(read)`; that form returns the value, **not** a removal handle. There is currently no `scope.remove(read)` by callback identity.
|
|
449
|
+
|
|
450
|
+
Both `subscribe()` and `clear()` work with either retention policy. `clear()` does not dispose the scope or its parent observer.
|
|
451
|
+
|
|
452
|
+
### Pending, refetches and errors
|
|
453
|
+
|
|
454
|
+
The parent effect/memo tracks the bridge, rather than the registered sources directly. The bridge observes the selected readers and conveys their changes, pending state and ordinary errors to Solid on the **next parent computation**.
|
|
455
|
+
|
|
456
|
+
- **Initial pending:** an async memo without a value can throw `NotReadyError` and hold the parent's next application.
|
|
457
|
+
- **Multiple pending readers:** A and B are both visited; one resolving does not make the parent ready while the other is pending.
|
|
458
|
+
- **Refetch with an old value:** the derived bridge carries Solid's pending verdict even when the async source can return a previous value.
|
|
459
|
+
- **Ordinary errors:** errors from registered readers can propagate through the bridge to the parent / Solid error handling. A registration that synchronously throws an ordinary error rolls back rather than leaving partial subscriptions.
|
|
460
|
+
- **Once pending ends:** a subsequent successful parent computation can apply normally.
|
|
461
|
+
|
|
462
|
+
Pending delegation is **not retroactive**: registering a pending dependency after an effect's apply callback has already executed cannot undo that callback. This is still experimental pending/transition integration; full scheduler-lane and Loading-boundary parity with ordinary in-effect reads is not guaranteed.
|
|
463
|
+
|
|
464
|
+
### Advanced: defer a pending check until after conditional cleanup
|
|
465
|
+
|
|
466
|
+
Normally `createScope()` checks the bridge during the parent's computation. If the bridge is already pending, `createScope()` can throw before later lines in the same computation run.
|
|
467
|
+
|
|
468
|
+
When your computation must first clear a retained registration, opt into `deferPending`:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { createEffect, createSignal } from 'solid-js';
|
|
472
|
+
import { createScope } from 'solid-deps/delegated';
|
|
473
|
+
|
|
474
|
+
const [enabled] = createSignal(true);
|
|
475
|
+
|
|
476
|
+
createEffect(
|
|
477
|
+
() => {
|
|
478
|
+
const active = enabled();
|
|
479
|
+
const scope = createScope({ retain: true, deferPending: true });
|
|
480
|
+
|
|
481
|
+
if (!active) scope.clear();
|
|
482
|
+
|
|
483
|
+
return scope.finish(active); // mandatory with deferPending: true
|
|
484
|
+
},
|
|
485
|
+
value => console.log('applied:', value)
|
|
486
|
+
);
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`finish(value)` reads the bridge and returns `value` unchanged if ready. It can throw pending/error status just as the normal check would. **`deferPending` is advanced and opt-in**: if enabled, call `scope.finish(...)` on **every computation pass**. Without it, pending propagation is not guaranteed. Neither `finish()` nor `clear()` is required in the ordinary usage shown above.
|
|
490
|
+
|
|
491
|
+
### Owner lifetime and safety
|
|
492
|
+
|
|
493
|
+
- Call `createScope()` inside a *tracked computation* of a Solid 2 effect or memo, not at module scope, outside tracking, or inside an effect's application callback.
|
|
494
|
+
- The same owner gets the same cached scope on normal recomputations. A late callback can register through that cached scope while the owner is alive.
|
|
495
|
+
- If the parent stops requesting its scope, the delegated root is retired. Disposal also releases its subscriptions. A subsequent call against a disposed scope raises `DisposedScopeError`.
|
|
496
|
+
- A stale async continuation can register on the **newest pass of the same live owner**; it is not automatically cancelled. Use your own cancellation/token guards if older async work must not attach.
|
|
497
|
+
- A callback executes under the external delegation mechanism, **not** under the original observer's ownership. Treat it as a pure read, not a way to create owned children or cleanups.
|
|
498
|
+
- The scope uses a single bridge for the parent; its internal implementation may use other reactive observers to capture late subscriptions.
|
|
499
|
+
|
|
500
|
+
### API reference
|
|
501
|
+
|
|
502
|
+
| API | Result | Role |
|
|
503
|
+
| --- | --- | --- |
|
|
504
|
+
| `createScope(options?)` | `DelegatedScope` | Get a stable scope for the currently tracked observer |
|
|
505
|
+
| `scope(read)` | `T` | Read and register one synchronous callback |
|
|
506
|
+
| `scope.batch(reads)` | Typed tuple | Read and register several callbacks together |
|
|
507
|
+
| `scope.subscribe(read)` | `() => void` | Add a removable registration |
|
|
508
|
+
| `unsubscribe()` | `void` | Remove only that registration; idempotent |
|
|
509
|
+
| `scope.clear()` | `void` | Remove all registrations without disposing the scope |
|
|
510
|
+
| `scope.finish(value)` | `T` | Perform the deferred bridge pending check; return the input |
|
|
511
|
+
| `scope.stats()` | `DelegatedScopeStats`-shaped object | Get diagnostic state/counters |
|
|
512
|
+
| `new DisposedScopeError()` | `Error` | Error class used for disposed/retired scopes |
|
|
513
|
+
|
|
514
|
+
#### `createScope` options
|
|
515
|
+
|
|
516
|
+
| Option | Type | Default | Purpose |
|
|
517
|
+
| --- | --- | --- | --- |
|
|
518
|
+
| `retain` | `boolean` | `false` | Preserve registrations across parent passes |
|
|
519
|
+
| `deferPending` | `boolean` | `false` | Move bridge check from `createScope()` to `scope.finish(value)` |
|
|
520
|
+
|
|
521
|
+
#### `scope.stats()` diagnostics
|
|
522
|
+
|
|
523
|
+
`alive` (boolean), `retain` (boolean), `readers` (number of current registered callbacks), `arms` (reaction re-arming count), `probes` (callback evaluations), `notifications` (source-event notification count), and `activations` (scope capture invocations). Counters are diagnostics, **not a performance API or guaranteed number of effect applications**.
|
|
524
|
+
|
|
525
|
+
### Full TypeScript declarations
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
/** Callable delegated scope bound to one Solid 2 tracking observer. */
|
|
529
|
+
export interface DelegatedScope {
|
|
530
|
+
/** Register a pure synchronous reader; may throw NotReadyError after capture. */
|
|
531
|
+
<T>(read: () => T): T;
|
|
532
|
+
|
|
533
|
+
/** Register independent readers as a typed tuple. */
|
|
534
|
+
batch<const T extends readonly (() => unknown)[]>(
|
|
535
|
+
reads: T
|
|
536
|
+
): { [K in keyof T]: ReturnType<T[K]> };
|
|
537
|
+
|
|
538
|
+
/** Return an idempotent unsubscribe function for this registration. */
|
|
539
|
+
subscribe(read: () => unknown): () => void;
|
|
540
|
+
|
|
541
|
+
/** Remove all registered readers without disposing this scope. */
|
|
542
|
+
clear(): void;
|
|
543
|
+
|
|
544
|
+
/** Required each pass when using { deferPending: true }. */
|
|
545
|
+
finish<T>(value: T): T;
|
|
546
|
+
|
|
547
|
+
/** Diagnostic state and implementation-specific counters. */
|
|
548
|
+
stats(): {
|
|
549
|
+
readonly alive: boolean;
|
|
550
|
+
readonly retain: boolean;
|
|
551
|
+
readonly readers: number;
|
|
552
|
+
readonly arms: number;
|
|
553
|
+
readonly probes: number;
|
|
554
|
+
readonly notifications: number;
|
|
555
|
+
readonly activations: number;
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export declare class DisposedScopeError extends Error {
|
|
560
|
+
constructor();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export declare function createScope(options?: {
|
|
564
|
+
retain?: boolean;
|
|
565
|
+
deferPending?: boolean;
|
|
566
|
+
}): DelegatedScope;
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The module has no default export. The declared interface can be imported via `import type { DelegatedScope } from 'solid-deps/delegated'`.
|
|
570
|
+
|
|
571
|
+
### Compatibility and limitations
|
|
572
|
+
|
|
573
|
+
Target: Solid 2 with `@solidjs/signals` compatible with `>=2.0.0-rc.17 <3.0.0`. Tested against rc.17 and rc.18 in production, development and observe builds; selected scenarios have also run in Chromium. Browser bundler behavior, complete Loading/transition parity, and future Solid internals are not guaranteed. Because registration can occur after a computation has completed, delegation is not always observably identical to having read every source synchronously inside the original effect.
|
|
574
|
+
|
|
575
|
+
## Graph-based behavior and limitations
|
|
277
576
|
|
|
278
577
|
- An attached read affects **future** invalidations. Writes that occurred before registration are not replayed.
|
|
279
578
|
- Extra dependencies can be removed on the target's next normal recomputation. Reattach them on each async effect execution when needed.
|
|
280
579
|
- `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.
|
|
281
580
|
- A callback that allocates owned children/cleanups, or reads a newly allocated probe-owned memo, is rejected. Access existing independently owned memos instead.
|
|
282
581
|
- 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.
|
|
283
|
-
-
|
|
582
|
+
- When using `Loading`, its **content computation must also read the pending memo under native tracking**. The scope only subscribes its target observer. In narrow rc.17 tests, explicitly mirroring the memo accessor inside Loading or separately subscribing the boundary's content observer *after* its tracked pass allows it to recover. The latter is advanced, experimental behavior and is not an automatic feature. Reading the accessor again is not the same as replaying the entire scoped callback.
|
|
583
|
+
- A late graft of a pending memo **does not put an already-running target effect into Solid's native pending state**, and does not retroactively defer an effect phase that has already executed. If a normal target recomputation prunes that late subscription before settlement, the memo will no longer wake it. Only live subscriptions can receive the settlement notification.
|
|
584
|
+
- 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. In particular, if a `Loading` boundary only reads a pending memo through `attachRead()` or `scope()`, it may show fallback indefinitely even after the memo resolves: the pending dependency belongs to the target effect, not necessarily to the boundary. Make sure the boundary itself performs its own normal tracked read of the pending memo, or manage boundary state separately. Advanced suspense/transition and held-transaction behavior is not broadly supported.
|
|
284
585
|
- 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
586
|
- Do not graft reads from inside effect-phase cleanup; use cleanup to cancel obsolete external work. Final disposal invalidates both scope types.
|
|
286
587
|
- 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.
|
|
@@ -289,7 +590,48 @@ With `createScope()`, a callback returning `number` gives a `number` directly. W
|
|
|
289
590
|
|
|
290
591
|
## Online Solid playground check
|
|
291
592
|
|
|
292
|
-
A copy-pasteable `main.tsx` is available in the source ZIP at `examples/online-playground/main.tsx`. Install `solid-deps@0.0.
|
|
593
|
+
A copy-pasteable `main.tsx` is available in the source ZIP at `examples/online-playground/main.tsx`. Install `solid-deps@0.0.6`, 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.
|
|
594
|
+
|
|
595
|
+
## Opt-in native pending parity: `createRetainedScope()`
|
|
596
|
+
|
|
597
|
+
A late dependency attached using `createScope()` or `attachRead()` can be pruned when the original computation reruns without reading it. For pending async memos, this may allow the effect to apply before every pending dependency has resolved.
|
|
598
|
+
|
|
599
|
+
`createRetainedScope()` offers a different approach: **declare pure accessors that Solid should read during every normal tracking pass**. The original effect executes those reads itself, letting Solid manage `NotReadyError`, Loading, pending sources, async settlement and dependency reconciliation normally. No private graph-link surgery is used by this API.
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
import { createEffect, createMemo } from 'solid-js';
|
|
603
|
+
import { createRetainedScope } from 'solid-deps';
|
|
604
|
+
|
|
605
|
+
const profile = createMemo(async () => loadProfile());
|
|
606
|
+
let retained;
|
|
607
|
+
|
|
608
|
+
createEffect(
|
|
609
|
+
() => {
|
|
610
|
+
// IMPORTANT: execute on every effect computation, not just once.
|
|
611
|
+
retained = createRetainedScope();
|
|
612
|
+
return 1;
|
|
613
|
+
},
|
|
614
|
+
() => { console.log('Effect applied after dependencies become ready'); }
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
// In a later event handler or async continuation, outside a tracked scope:
|
|
618
|
+
const unsubscribe = retained.subscribe(() => profile());
|
|
619
|
+
// The callback is NOT executed here. Solid recomputes the original effect,
|
|
620
|
+
// reads profile() natively, and suspends/resumes normally if it is NotReady.
|
|
621
|
+
|
|
622
|
+
// Later, to remove this retained read:
|
|
623
|
+
unsubscribe();
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Usage requirements:**
|
|
627
|
+
|
|
628
|
+
- Call `createRetainedScope()` once on **every** execution of the original tracked compute function. It returns a stable registration handle for that same computation.
|
|
629
|
+
- Call `retained.subscribe(accessor)` outside an owned or tracking context, for example in an event handler or after an `await` (not inside an effect cleanup). It schedules native recomputation and returns an idempotent unsubscribe function.
|
|
630
|
+
- Accessors must be **pure and synchronous**: they may read signals, stores and memos, but must not write state, register cleanup, create effects, or start async work. Don't pass an `async` function or a callback returning a Promise.
|
|
631
|
+
- The accessor is invoked on future effect computations, potentially many times. It is not invoked during registration. Disposal removes native subscriptions, and registering after disposal throws.
|
|
632
|
+
- Unlike `createScope()` and `createOwnerScope()`, registration does **not** return the memo's immediate value. It's a subscription operation that asks the original computation to read the value naturally.
|
|
633
|
+
|
|
634
|
+
This API is opt-in and experimental. It requires cooperation from the original effect's compute function. It cannot retroactively undo an effect phase that already applied before registration, but it can trigger a new normal computation whose pending state is handled by Solid itself. The existing scope APIs remain unchanged.
|
|
293
635
|
|
|
294
636
|
## License
|
|
295
637
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-deps",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Experimental
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "Experimental Solid 2 late-dependency tracking and hybrid owner fallback",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
@@ -11,16 +11,24 @@
|
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./types/index.d.mts",
|
|
13
13
|
"default": "./src/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./delegated": {
|
|
16
|
+
"types": "./types/delegated.d.mts",
|
|
17
|
+
"default": "./src/delegated-scope.mjs"
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"types": "./types/index.d.mts",
|
|
17
21
|
"files": [
|
|
18
22
|
"src/attach-read.mjs",
|
|
19
23
|
"src/discover.mjs",
|
|
24
|
+
"src/retained-scope.mjs",
|
|
20
25
|
"src/index.mjs",
|
|
26
|
+
"src/hybrid-scope.mjs",
|
|
21
27
|
"types/index.d.mts",
|
|
22
28
|
"README.md",
|
|
23
|
-
"LICENSE"
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"src/delegated-scope.mjs",
|
|
31
|
+
"types/delegated.d.mts"
|
|
24
32
|
],
|
|
25
33
|
"sideEffects": false,
|
|
26
34
|
"peerDependencies": {
|
|
@@ -37,17 +45,34 @@
|
|
|
37
45
|
],
|
|
38
46
|
"scripts": {
|
|
39
47
|
"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 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",
|
|
48
|
+
"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 test/test-loading-suspension.mjs test/test-loading-recovery-research.mjs test/test-native-notready-parity.mjs test/test-pending-status-propagation.mjs test/test-retained-scope.mjs test/test-research-retained.mjs test/test-parallel-parity.mjs test/test-batched-reads.mjs test/test-direct-links-research.mjs test/test-delegated-scope-research.mjs test/test-owner-cached-delegated.mjs test/test-sharded-delegated.mjs test/test-memory-conformance.mjs test/test-optimized-delegated.mjs test/test-unified-scope-contract.mjs test/test-unified-advanced-contract.mjs test/test-delegated-advanced-contract.mjs test/test-delegated-retention-contract.mjs test/test-delegated-default-dev19.mjs test/test-delegated-pending-bridge-dev25.mjs test/test-hybrid-scope.mjs",
|
|
49
|
+
"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 test/test-loading-suspension.mjs test/test-loading-recovery-research.mjs test/test-native-notready-parity.mjs test/test-pending-status-propagation.mjs test/test-retained-scope.mjs test/test-research-retained.mjs test/test-parallel-parity.mjs test/test-batched-reads.mjs test/test-direct-links-research.mjs test/test-delegated-scope-research.mjs test/test-owner-cached-delegated.mjs test/test-sharded-delegated.mjs test/test-memory-conformance.mjs test/test-optimized-delegated.mjs test/test-unified-scope-contract.mjs test/test-unified-advanced-contract.mjs test/test-delegated-advanced-contract.mjs test/test-delegated-retention-contract.mjs test/test-delegated-default-dev19.mjs test/test-delegated-pending-bridge-dev25.mjs test/test-hybrid-scope.mjs",
|
|
50
|
+
"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 test/test-loading-suspension.mjs test/test-loading-recovery-research.mjs test/test-native-notready-parity.mjs test/test-pending-status-propagation.mjs test/test-retained-scope.mjs test/test-research-retained.mjs test/test-parallel-parity.mjs test/test-batched-reads.mjs test/test-direct-links-research.mjs test/test-delegated-scope-research.mjs test/test-owner-cached-delegated.mjs test/test-sharded-delegated.mjs test/test-memory-conformance.mjs test/test-optimized-delegated.mjs test/test-unified-scope-contract.mjs test/test-unified-advanced-contract.mjs test/test-delegated-advanced-contract.mjs test/test-delegated-retention-contract.mjs test/test-delegated-default-dev19.mjs test/test-delegated-pending-bridge-dev25.mjs test/test-hybrid-scope.mjs",
|
|
43
51
|
"test:all": "node scripts/run-tests.mjs",
|
|
44
52
|
"test:package": "node scripts/smoke-package.mjs",
|
|
45
53
|
"test:browser": "node --test test/browser/playwright.test.mjs",
|
|
46
|
-
"test:browser:preflight": "node scripts/browser-preflight.mjs"
|
|
54
|
+
"test:browser:preflight": "node scripts/browser-preflight.mjs",
|
|
55
|
+
"bench:memory": "node --conditions=browser research/benchmark-conformance-memory.mjs",
|
|
56
|
+
"bench:memory:dev": "node --conditions=browser --conditions=development research/benchmark-conformance-memory.mjs",
|
|
57
|
+
"bench:memory:observe": "node --conditions=browser --conditions=observe research/benchmark-conformance-memory.mjs",
|
|
58
|
+
"test:unified-contract": "node --conditions=browser --test test/test-unified-scope-contract.mjs test/test-unified-advanced-contract.mjs test/test-delegated-retention-contract.mjs test/test-delegated-default-dev19.mjs && node --conditions=browser --conditions=development --test test/test-unified-scope-contract.mjs test/test-unified-advanced-contract.mjs test/test-delegated-retention-contract.mjs test/test-delegated-default-dev19.mjs && node --conditions=browser --conditions=observe --test test/test-unified-scope-contract.mjs test/test-unified-advanced-contract.mjs test/test-delegated-retention-contract.mjs test/test-delegated-default-dev19.mjs",
|
|
59
|
+
"bench:realistic": "node --conditions=browser --expose-gc research/benchmark-realistic-scopes-dev17.mjs",
|
|
60
|
+
"test:matrix": "node scripts/test-matrix.mjs",
|
|
61
|
+
"setup:browser": "node scripts/setup-browser.mjs",
|
|
62
|
+
"test:hybrid:browser": "node scripts/browser-hybrid-chromium.mjs --rc=17 && node scripts/browser-hybrid-chromium.mjs --rc=18 && node scripts/setup.mjs --rc=17",
|
|
63
|
+
"test:rc20": "node scripts/test-matrix.mjs --rc=20",
|
|
64
|
+
"test:hybrid:types": "tsc -p tsconfig.hybrid.json"
|
|
47
65
|
},
|
|
48
66
|
"devDependencies": {
|
|
49
67
|
"@solidjs/signals": "2.0.0-rc.17",
|
|
50
68
|
"solid-js": "2.0.0-rc.17",
|
|
51
69
|
"playwright-core": "^1.56.0"
|
|
70
|
+
},
|
|
71
|
+
"typesVersions": {
|
|
72
|
+
"*": {
|
|
73
|
+
"delegated": [
|
|
74
|
+
"types/delegated.d.mts"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
52
77
|
}
|
|
53
78
|
}
|
package/src/attach-read.mjs
CHANGED
|
@@ -174,6 +174,132 @@ export function attachRead(observerOrCycle, read) {
|
|
|
174
174
|
disposeRoot?.();
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* One temporary observer + one target graph reconciliation for several reads.
|
|
179
|
+
* Each accessor is tried independently: NotReadyError preserves what it tracked
|
|
180
|
+
* and does not stop later readers. The first NotReadyError is rethrown AFTER
|
|
181
|
+
* all valid dependency links have been transferred. Any ordinary exception or
|
|
182
|
+
* thenable return aborts the whole batch before touching the target graph.
|
|
183
|
+
*
|
|
184
|
+
* This is NOT execution under the target's native tracking context; pending
|
|
185
|
+
* status and Loading semantics are unchanged from attachRead().
|
|
186
|
+
*/
|
|
187
|
+
export function attachReads(observerOrCycle, readers) {
|
|
188
|
+
if (!Array.isArray(readers)) throw new TypeError('reads must be an array of synchronous functions');
|
|
189
|
+
const reads = readers.slice();
|
|
190
|
+
if (reads.some(read => typeof read !== 'function'))
|
|
191
|
+
throw new TypeError('each read must be a function');
|
|
192
|
+
const pass = observerOrCycle && ownerCycles.get(observerOrCycle);
|
|
193
|
+
const target = pass ? pass.observer : observerOrCycle;
|
|
194
|
+
const s = layoutOf(target);
|
|
195
|
+
const skipped = (disposed, stale) => ({
|
|
196
|
+
values: undefined, added: 0, duplicate: 0, promoted: 0, disposed, stale
|
|
197
|
+
});
|
|
198
|
+
if (isDisposed(target)) return skipped(true, false);
|
|
199
|
+
if (pass && target[s.gen] !== pass.generation) return skipped(false, true);
|
|
200
|
+
if (target[s.flags] & s.recomputingBit) throw new Error('Target observer is currently recomputing');
|
|
201
|
+
if (!reads.length) return {
|
|
202
|
+
values: [], added: 0, duplicate: 0, promoted: 0, disposed: false, stale: false
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const existing = links(target, s);
|
|
206
|
+
const beforeTail = target[s.tail];
|
|
207
|
+
const beforeGen = target[s.gen];
|
|
208
|
+
const prefixEnd = beforeTail == null ? -1 : existing.indexOf(beforeTail);
|
|
209
|
+
if (beforeTail !== null && prefixEnd < 0)
|
|
210
|
+
throw new Error('Target tail is not reachable from its dependency head');
|
|
211
|
+
const valid = existing.slice(0, prefixEnd + 1);
|
|
212
|
+
const stale = existing.slice(prefixEnd + 1);
|
|
213
|
+
const values = new Array(reads.length);
|
|
214
|
+
let disposeRoot;
|
|
215
|
+
let probe;
|
|
216
|
+
let pendingError;
|
|
217
|
+
let fatalError;
|
|
218
|
+
let failed = false;
|
|
219
|
+
try {
|
|
220
|
+
createRoot(dispose => {
|
|
221
|
+
disposeRoot = dispose;
|
|
222
|
+
const track = createReaction(() => {});
|
|
223
|
+
track(() => {
|
|
224
|
+
probe = getObserver();
|
|
225
|
+
for (let i = 0; i < reads.length; i++) {
|
|
226
|
+
try {
|
|
227
|
+
const value = reads[i]();
|
|
228
|
+
if (value != null && typeof value.then === 'function')
|
|
229
|
+
throw new TypeError('attachReads requires synchronous reads, not an async callback');
|
|
230
|
+
values[i] = value;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof NotReadyError) pendingError ??= error;
|
|
233
|
+
else { fatalError = error; failed = true; break; }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
if (failed) throw fatalError;
|
|
239
|
+
if (!probe) throw new Error('No temporary observer was captured');
|
|
240
|
+
const p = layoutOf(probe);
|
|
241
|
+
if (p !== s) throw new Error('Temporary observer and target use different Solid runtimes');
|
|
242
|
+
if (isDisposed(target)) return skipped(true, false);
|
|
243
|
+
if ((target[s.flags] & s.recomputingBit) || target[s.tail] !== beforeTail || target[s.gen] !== beforeGen ||
|
|
244
|
+
(target[s.deps] ?? null) !== (existing[0] ?? null))
|
|
245
|
+
throw new Error('Target changed during capture; refusing unsafe graft');
|
|
246
|
+
const after = links(target, s);
|
|
247
|
+
if (after.length !== existing.length || after.some((edge, i) => edge !== existing[i]))
|
|
248
|
+
throw new Error('Target dependency chain changed during capture');
|
|
249
|
+
|
|
250
|
+
const captured = links(probe, s);
|
|
251
|
+
if (probe[s.child] != null || probe[s.disposal] != null)
|
|
252
|
+
throw new Error('attachReads read() cannot create owned children or register cleanups');
|
|
253
|
+
for (const edge of captured) {
|
|
254
|
+
const source = edge[s.dep];
|
|
255
|
+
if (isDisposed(source) || ownedBy(source, probe))
|
|
256
|
+
throw new Error('Cannot attach a disposed or temporary-observer-owned source');
|
|
257
|
+
}
|
|
258
|
+
const seen = new Set(valid.map(edge => edge[s.dep]));
|
|
259
|
+
const staleBySource = new Map(stale.map(edge => [edge[s.dep], edge]));
|
|
260
|
+
const moved = [];
|
|
261
|
+
const promoted = [];
|
|
262
|
+
const kept = [];
|
|
263
|
+
for (const edge of captured) {
|
|
264
|
+
const source = edge[s.dep];
|
|
265
|
+
if (seen.has(source)) {
|
|
266
|
+
kept.push(edge);
|
|
267
|
+
} else {
|
|
268
|
+
seen.add(source);
|
|
269
|
+
const old = staleBySource.get(source);
|
|
270
|
+
if (old) {
|
|
271
|
+
promoted.push(old);
|
|
272
|
+
kept.push(edge);
|
|
273
|
+
} else moved.push(edge);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
probe[s.deps] = kept[0] ?? null;
|
|
277
|
+
probe[s.tail] = kept.at(-1) ?? null;
|
|
278
|
+
for (let i = 0; i < kept.length; i++) kept[i][s.next] = kept[i + 1] ?? null;
|
|
279
|
+
|
|
280
|
+
const promotedSet = new Set(promoted);
|
|
281
|
+
const staleRemainder = stale.filter(edge => !promotedSet.has(edge));
|
|
282
|
+
const additions = [...promoted, ...moved];
|
|
283
|
+
const validated = [...valid, ...additions];
|
|
284
|
+
const nextList = [...validated, ...staleRemainder];
|
|
285
|
+
for (const edge of additions) {
|
|
286
|
+
edge[s.sub] = target;
|
|
287
|
+
edge[s.linkGen] = beforeGen;
|
|
288
|
+
}
|
|
289
|
+
for (let i = 0; i < nextList.length; i++)
|
|
290
|
+
nextList[i][s.next] = nextList[i + 1] ?? null;
|
|
291
|
+
target[s.deps] = nextList[0] ?? null;
|
|
292
|
+
target[s.tail] = validated.at(-1) ?? null;
|
|
293
|
+
if (pendingError) throw pendingError;
|
|
294
|
+
return {
|
|
295
|
+
values, added: additions.length, duplicate: kept.length - promoted.length,
|
|
296
|
+
promoted: promoted.length, disposed: false, stale: false
|
|
297
|
+
};
|
|
298
|
+
} finally {
|
|
299
|
+
disposeRoot?.();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
177
303
|
export function inspectDeps(owner) {
|
|
178
304
|
const s = layoutOf(owner);
|
|
179
305
|
return links(owner, s).map(link => link[s.dep]);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental public-Solid-API delegated scope: one derived bridge per owner.
|
|
3
|
+
* A reaction captures external subscriptions synchronously; the parent observes
|
|
4
|
+
* only one memo carrying changes, pending state and ordinary errors. Registration
|
|
5
|
+
* is not an event and cannot itself trigger an extra parent application.
|
|
6
|
+
* No internal Solid graph links are inspected or mutated.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
createRoot, createReaction, createMemo, createSignal, getObserver, isDisposed,
|
|
10
|
+
onCleanup, runWithOwner, untrack, NotReadyError
|
|
11
|
+
} from '@solidjs/signals';
|
|
12
|
+
|
|
13
|
+
const scopes = new WeakMap();
|
|
14
|
+
export class DisposedScopeError extends Error {
|
|
15
|
+
constructor() { super('The scope owner has been disposed'); this.name='DisposedScopeError'; }
|
|
16
|
+
}
|
|
17
|
+
const isThenable=x=>x!=null&&typeof x.then==='function';
|
|
18
|
+
function check(value){if(isThenable(value))throw new TypeError('Scope callbacks must be synchronous');return value;}
|
|
19
|
+
function guard(state){if(!state.alive||isDisposed(state.observer))throw new DisposedScopeError();}
|
|
20
|
+
|
|
21
|
+
function allocate(observer,retain,deferPending){
|
|
22
|
+
const state={observer,retain,deferPending,alive:true,activations:0,readers:[],arms:0,probes:0,notifications:0,
|
|
23
|
+
dispose:null,scope:null,bridge:null,markRegistration:null,arm:null,rearm:null,event:null};
|
|
24
|
+
runWithOwner(null,()=>createRoot(dispose=>{
|
|
25
|
+
state.dispose=dispose;
|
|
26
|
+
const [registration,bumpRegistration]=createSignal(0,{ownedWrite:true});
|
|
27
|
+
const [events,bumpEvents]=createSignal(0,{ownedWrite:true});
|
|
28
|
+
state.markRegistration=()=>bumpRegistration(n=>n+1);
|
|
29
|
+
state.event=()=>{state.notifications++;bumpEvents(n=>n+1);};
|
|
30
|
+
state.arm=createReaction(()=>{
|
|
31
|
+
if(!state.alive||isDisposed(observer))return;
|
|
32
|
+
state.rearm();
|
|
33
|
+
state.event();
|
|
34
|
+
});
|
|
35
|
+
state.rearm=()=>{
|
|
36
|
+
if(!state.alive)return;
|
|
37
|
+
state.arms++;
|
|
38
|
+
const copy=state.readers.slice();
|
|
39
|
+
state.arm(()=>{
|
|
40
|
+
for(const item of copy){
|
|
41
|
+
if(!item.active)continue;
|
|
42
|
+
state.probes++;
|
|
43
|
+
try{check(item.read());}catch(error){
|
|
44
|
+
// Continue capturing the remaining readers even if one suspends or
|
|
45
|
+
// fails. The coherent memo carries pending/errors to the parent.
|
|
46
|
+
item.error=error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
state.bridge=createMemo(()=>{
|
|
52
|
+
registration();
|
|
53
|
+
const version=events();
|
|
54
|
+
let pending;
|
|
55
|
+
for(const item of state.readers){
|
|
56
|
+
if(!item.active)continue;
|
|
57
|
+
state.probes++;
|
|
58
|
+
try{check(untrack(item.read));}
|
|
59
|
+
catch(error){if(error instanceof NotReadyError)pending??=error;else throw error;}
|
|
60
|
+
}
|
|
61
|
+
if(pending)throw pending;
|
|
62
|
+
return version;
|
|
63
|
+
});
|
|
64
|
+
}));
|
|
65
|
+
state.scope=makeScope(state);scopes.set(observer,state);
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
function destroy(state){
|
|
69
|
+
if(!state.alive)return;
|
|
70
|
+
state.alive=false;state.readers.length=0;scopes.delete(state.observer);state.dispose();
|
|
71
|
+
}
|
|
72
|
+
function retire(state,activation){
|
|
73
|
+
if(!state.alive||state.retain)return;
|
|
74
|
+
const before=state.readers.length;
|
|
75
|
+
state.readers=state.readers.filter(item=>{
|
|
76
|
+
if(item.activation>activation)return true;
|
|
77
|
+
item.active=false;return false;
|
|
78
|
+
});
|
|
79
|
+
if(state.readers.length!==before){state.rearm();state.markRegistration();}
|
|
80
|
+
}
|
|
81
|
+
function publishRegistration(state){state.rearm();state.markRegistration();}
|
|
82
|
+
function makeScope(state){
|
|
83
|
+
function insert(readers,throwPending,asSubscription=false){
|
|
84
|
+
guard(state);
|
|
85
|
+
if(readers.some(read=>typeof read!=='function'))throw new TypeError('Scope read must be a function');
|
|
86
|
+
// Stage and validate before subscribing, and never leak a partially registered batch.
|
|
87
|
+
const values=[];let pending;
|
|
88
|
+
for(const read of readers){
|
|
89
|
+
try{values.push(check(untrack(read)));}
|
|
90
|
+
catch(error){if(!(error instanceof NotReadyError))throw error;pending??=error;values.push(undefined);}
|
|
91
|
+
}
|
|
92
|
+
const items=readers.map(read=>({read,active:true,activation:state.activations}));
|
|
93
|
+
state.readers.push(...items);
|
|
94
|
+
publishRegistration(state);
|
|
95
|
+
if(throwPending&&pending)throw pending;
|
|
96
|
+
return asSubscription?items:values;
|
|
97
|
+
}
|
|
98
|
+
function scope(read){return insert([read],true)[0];}
|
|
99
|
+
scope.batch=(readers)=>{
|
|
100
|
+
guard(state);
|
|
101
|
+
if(!Array.isArray(readers))throw new TypeError('Scope batch requires an array of functions');
|
|
102
|
+
if(!readers.length)return [];
|
|
103
|
+
return insert(readers,true);
|
|
104
|
+
};
|
|
105
|
+
scope.subscribe=read=>{
|
|
106
|
+
const [item]=insert([read],false,true);
|
|
107
|
+
let stopped=false;
|
|
108
|
+
return ()=>{
|
|
109
|
+
if(stopped)return;stopped=true;
|
|
110
|
+
if(!state.alive)return;
|
|
111
|
+
item.active=false;
|
|
112
|
+
const idx=state.readers.indexOf(item);
|
|
113
|
+
if(idx>=0)state.readers.splice(idx,1);
|
|
114
|
+
publishRegistration(state);
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
// Optional end-of-computation check for branches that must clear a pending
|
|
118
|
+
// retained scope before reading its verdict.
|
|
119
|
+
scope.finish=value=>{guard(state);state.bridge();return value;};
|
|
120
|
+
scope.clear=()=>{
|
|
121
|
+
guard(state);
|
|
122
|
+
if(state.readers.length){
|
|
123
|
+
for(const item of state.readers)item.active=false;
|
|
124
|
+
state.readers.length=0;publishRegistration(state);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
scope.stats=()=>({alive:state.alive,retain:state.retain,readers:state.readers.length,
|
|
128
|
+
arms:state.arms,probes:state.probes,notifications:state.notifications,activations:state.activations});
|
|
129
|
+
return scope;
|
|
130
|
+
}
|
|
131
|
+
export function createScope(options){
|
|
132
|
+
const retain=options?.retain??false;
|
|
133
|
+
const deferPending=options?.deferPending??false;
|
|
134
|
+
if(options!=null&&(typeof options!=='object'||Array.isArray(options)||typeof retain!=='boolean'||typeof deferPending!=='boolean'||Object.keys(options).some(k=>k!=='retain'&&k!=='deferPending')))
|
|
135
|
+
throw new TypeError('createScope options.retain and options.deferPending must be booleans');
|
|
136
|
+
const observer=getObserver();if(!observer)throw new Error('createScope() requires a tracking effect or memo');
|
|
137
|
+
let state=scopes.get(observer);
|
|
138
|
+
if(!state||!state.alive)state=allocate(observer,retain,deferPending);
|
|
139
|
+
else if(state.retain!==retain||state.deferPending!==deferPending)throw new Error('createScope retain/deferPending option cannot change for an existing observer');
|
|
140
|
+
const activation=++state.activations;
|
|
141
|
+
onCleanup(()=>{
|
|
142
|
+
if(isDisposed(observer)){destroy(state);return;}
|
|
143
|
+
retire(state,activation);
|
|
144
|
+
queueMicrotask(()=>{
|
|
145
|
+
if(state.alive&&!isDisposed(observer)&&state.activations===activation)destroy(state);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
state.rearm();
|
|
149
|
+
state.markRegistration();
|
|
150
|
+
if(!state.deferPending)state.bridge();
|
|
151
|
+
return state.scope;
|
|
152
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One factory for two explicitly separate execution contexts:
|
|
3
|
+
* - tracked computation: attach reads to its captured observer cycle;
|
|
4
|
+
* - plain owned scope: re-enter its owner without reactive attachment.
|
|
5
|
+
*
|
|
6
|
+
* Never mix both semantics in a single invocation. An explicitly supplied
|
|
7
|
+
* observer retains the legacy strict observer-only behavior.
|
|
8
|
+
*/
|
|
9
|
+
import { getObserver, getOwner, isDisposed, runWithOwner } from '@solidjs/signals';
|
|
10
|
+
import {
|
|
11
|
+
createScope as createTrackedScope,
|
|
12
|
+
DisposedScopeError
|
|
13
|
+
} from './attach-read.mjs';
|
|
14
|
+
|
|
15
|
+
/** Original strict, cycle-bound, observer-only factory. */
|
|
16
|
+
export const createObserverScope = createTrackedScope;
|
|
17
|
+
|
|
18
|
+
function ownerFallback(owner) {
|
|
19
|
+
return function scope(callback) {
|
|
20
|
+
if (typeof callback !== 'function') throw new TypeError('scope requires a synchronous callback');
|
|
21
|
+
if (isDisposed(owner)) throw new DisposedScopeError();
|
|
22
|
+
const value = runWithOwner(owner, callback);
|
|
23
|
+
if (value != null && typeof value.then === 'function')
|
|
24
|
+
throw new TypeError('scope callback must be synchronous; split at await before calling scope');
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Capture either the current tracking observer or, if none, the current owner.
|
|
31
|
+
* An explicit target preserves the legacy createScope(observer) contract:
|
|
32
|
+
* it MUST be a tracking Computed, not a plain owner.
|
|
33
|
+
*/
|
|
34
|
+
export function createHybridScope(owner) {
|
|
35
|
+
if (arguments.length !== 0) return createTrackedScope(owner);
|
|
36
|
+
const observer = getObserver();
|
|
37
|
+
if (observer) return createTrackedScope(observer);
|
|
38
|
+
const capturedOwner = getOwner();
|
|
39
|
+
if (!capturedOwner) {
|
|
40
|
+
throw new Error('createScope requires an active Solid tracking observer or owner');
|
|
41
|
+
}
|
|
42
|
+
if (isDisposed(capturedOwner)) throw new DisposedScopeError();
|
|
43
|
+
return ownerFallback(capturedOwner);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Default factory for owner-scope transforms. */
|
|
47
|
+
export const createScope = createHybridScope;
|
package/src/index.mjs
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createRetainedScope } from './retained-scope.mjs';
|
|
2
|
+
export { createScope, createHybridScope, createObserverScope } from './hybrid-scope.mjs';
|
|
3
|
+
export { attachRead, attachReads, captureOwnerCycle, createOwnerScope, inspectDeps, StaleScopeError, DisposedScopeError } from './attach-read.mjs';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in native replay of pure reactive accessors.
|
|
3
|
+
*
|
|
4
|
+
* Unlike attachRead(), this does not transfer graph links. It asks the
|
|
5
|
+
* original tracking function to read declared accessors on its next pass,
|
|
6
|
+
* letting Solid perform native NotReady, Loading, status and lane accounting.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Call createRetainedScope() during EVERY run of the original
|
|
9
|
+
* tracked computation; accessor functions passed to subscribe() must be
|
|
10
|
+
* synchronous and free of writes, cleanup registration or other side effects.
|
|
11
|
+
*/
|
|
12
|
+
import { createSignal, getObserver, getOwner, isDisposed } from '@solidjs/signals';
|
|
13
|
+
|
|
14
|
+
import { DisposedScopeError } from './attach-read.mjs';
|
|
15
|
+
|
|
16
|
+
const scopes = new WeakMap();
|
|
17
|
+
|
|
18
|
+
export function createRetainedScope() {
|
|
19
|
+
const owner = getObserver();
|
|
20
|
+
if (!owner) throw new Error('createRetainedScope() requires an active tracking computation');
|
|
21
|
+
let state = scopes.get(owner);
|
|
22
|
+
if (!state) {
|
|
23
|
+
const [revision, invalidate] = createSignal(0);
|
|
24
|
+
state = { reads: [], revision, invalidate };
|
|
25
|
+
const subscribe = read => {
|
|
26
|
+
if (typeof read !== 'function') throw new TypeError('subscribe() expects a synchronous accessor callback');
|
|
27
|
+
if (isDisposed(owner)) {
|
|
28
|
+
state.reads.length = 0;
|
|
29
|
+
throw new DisposedScopeError();
|
|
30
|
+
}
|
|
31
|
+
// An active foreign observer (including a nested effect) is not a safe
|
|
32
|
+
// place to write Solid signals, and the callback should not be replayed
|
|
33
|
+
// as part of another computation's setup.
|
|
34
|
+
if (getOwner()) throw new Error('subscribe() must be called outside an owned or tracking scope');
|
|
35
|
+
const item = { read };
|
|
36
|
+
state.reads.push(item);
|
|
37
|
+
// Trigger a normal compute pass. The callback does NOT execute here.
|
|
38
|
+
state.invalidate(n => n + 1);
|
|
39
|
+
let stopped = false;
|
|
40
|
+
return () => {
|
|
41
|
+
if (stopped) return;
|
|
42
|
+
const alive = !isDisposed(owner);
|
|
43
|
+
if (alive && getOwner()) throw new Error('Unsubscribe outside an owned or tracking scope');
|
|
44
|
+
stopped = true;
|
|
45
|
+
const index = state.reads.indexOf(item);
|
|
46
|
+
if (index !== -1) state.reads.splice(index, 1);
|
|
47
|
+
if (alive) state.invalidate(n => n + 1);
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
state.handle = Object.freeze({ subscribe });
|
|
51
|
+
scopes.set(owner, state);
|
|
52
|
+
}
|
|
53
|
+
// Read the revision in the effect's OWN active tracking pass.
|
|
54
|
+
state.revision();
|
|
55
|
+
// Snapshot avoids reentrant modifications changing iteration order.
|
|
56
|
+
// An exception (including NotReady) propagates to Solid's normal effect
|
|
57
|
+
// compute handler, preserving native suspension and error bookkeeping.
|
|
58
|
+
for (const item of [...state.reads]) {
|
|
59
|
+
const result = item.read();
|
|
60
|
+
if (result != null && typeof result.then === 'function')
|
|
61
|
+
throw new TypeError('Retained read callbacks must be synchronous (not return Promise/thenable)');
|
|
62
|
+
}
|
|
63
|
+
return state.handle;
|
|
64
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Callable delegated scope bound to one Solid 2 tracking observer. */
|
|
2
|
+
export interface DelegatedScope {
|
|
3
|
+
/**
|
|
4
|
+
* Register a pure synchronous reader and return its current result.
|
|
5
|
+
* A pending async source may throw NotReadyError, after registration.
|
|
6
|
+
*/
|
|
7
|
+
<T>(read: () => T): T;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register independent readers together and return their values as a tuple.
|
|
11
|
+
* Visits all readers even when one throws NotReadyError; an ordinary error
|
|
12
|
+
* aborts registration without leaving a partially added batch.
|
|
13
|
+
*/
|
|
14
|
+
batch<const T extends readonly (() => unknown)[]>(
|
|
15
|
+
reads: T
|
|
16
|
+
): { [K in keyof T]: ReturnType<T[K]> };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a reader and return an idempotent unsubscribe function.
|
|
20
|
+
* Use when an individual registration must be removable.
|
|
21
|
+
*/
|
|
22
|
+
subscribe(read: () => unknown): () => void;
|
|
23
|
+
|
|
24
|
+
/** Remove all current registrations; the scope itself stays reusable. */
|
|
25
|
+
clear(): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read the pending-aware bridge and return value unchanged.
|
|
29
|
+
* Must be called during each tracked pass when deferPending is true.
|
|
30
|
+
*/
|
|
31
|
+
finish<T>(value: T): T;
|
|
32
|
+
|
|
33
|
+
/** Diagnostics; counters are implementation-specific, not performance guarantees. */
|
|
34
|
+
stats(): {
|
|
35
|
+
readonly alive: boolean;
|
|
36
|
+
readonly retain: boolean;
|
|
37
|
+
readonly readers: number;
|
|
38
|
+
readonly arms: number;
|
|
39
|
+
readonly probes: number;
|
|
40
|
+
readonly notifications: number;
|
|
41
|
+
readonly activations: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Raised when a scope is used after its observer/scope has been disposed. */
|
|
46
|
+
export declare class DisposedScopeError extends Error {
|
|
47
|
+
constructor();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create or retrieve the scope cached for the current tracked effect/memo.
|
|
52
|
+
*
|
|
53
|
+
* - retain: false (default): per-pass reconciliation.
|
|
54
|
+
* - retain: true: registrations persist until explicitly removed/disposed.
|
|
55
|
+
* - deferPending: false (default): read the bridge in createScope().
|
|
56
|
+
* - deferPending: true: caller must read it with scope.finish(...) each pass.
|
|
57
|
+
*
|
|
58
|
+
* Each observer must use the same options across its lifetime.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createScope(options?: {
|
|
61
|
+
retain?: boolean;
|
|
62
|
+
deferPending?: boolean;
|
|
63
|
+
}): DelegatedScope;
|
package/types/index.d.mts
CHANGED
|
@@ -42,11 +42,22 @@ export declare class DisposedScopeError extends Error {
|
|
|
42
42
|
export declare function captureOwnerCycle(owner?: Owner | null): OwnerCycle;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
45
|
+
* Hybrid default: capture a tracking observer cycle when available, otherwise
|
|
46
|
+
* capture a plain Solid owner and restore it using runWithOwner() for each
|
|
47
|
+
* synchronous callback. The owner-only branch does not subscribe signal reads.
|
|
48
|
+
* An explicitly passed owner must still be a tracking Computed (the legacy
|
|
49
|
+
* createScope(observer) behavior); plain-owner fallback is implicit only.
|
|
50
|
+
* Calls after observer supersession throw StaleScopeError; calls after
|
|
51
|
+
* either owner is disposed throw DisposedScopeError.
|
|
47
52
|
*/
|
|
48
53
|
export declare function createScope(owner?: Owner | null): Scope;
|
|
49
54
|
|
|
55
|
+
/** Same implementation as createScope: explicit name for the hybrid factory. */
|
|
56
|
+
export declare function createHybridScope(owner?: Owner | null): Scope;
|
|
57
|
+
|
|
58
|
+
/** Strict legacy factory: requires a tracking observer, even under a plain owner. */
|
|
59
|
+
export declare function createObserverScope(owner?: Owner | null): Scope;
|
|
60
|
+
|
|
50
61
|
/**
|
|
51
62
|
* Create an owner-bound callable scope. Unlike createScope(), it stays valid
|
|
52
63
|
* across normal reruns of the same computation and rejects final disposal.
|
|
@@ -63,5 +74,43 @@ export declare function attachRead<T>(
|
|
|
63
74
|
read: () => T
|
|
64
75
|
): AttachReadResult<T>;
|
|
65
76
|
|
|
77
|
+
/** Result of a batched synchronous attachment, with values in input order. */
|
|
78
|
+
export interface AttachReadsResult<T> extends Omit<AttachReadResult<T>, 'value'> {
|
|
79
|
+
/** Undefined when target is disposed or its captured cycle is stale. */
|
|
80
|
+
readonly values: T | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Graft reads in a single temporary tracking pass and one graph reconciliation.
|
|
85
|
+
* A NotReadyError from one read does not skip subsequent reads; the FIRST
|
|
86
|
+
* NotReadyError is thrown after grafting all encountered dependencies.
|
|
87
|
+
* Other errors abort without changing the target dependency list.
|
|
88
|
+
* Returned values are aligned with the callbacks (undefined for pending reads).
|
|
89
|
+
* Like attachRead, this does not restore native pending/Loading context.
|
|
90
|
+
*/
|
|
91
|
+
export declare function attachReads<const T extends readonly (() => unknown)[]>(
|
|
92
|
+
observerOrCycle: Owner | OwnerCycle | null,
|
|
93
|
+
reads: T
|
|
94
|
+
): AttachReadsResult<{ [K in keyof T]: ReturnType<T[K]> }>;
|
|
95
|
+
|
|
66
96
|
/** Diagnostic only: get the raw dependency-source nodes for a computation. */
|
|
67
97
|
export declare function inspectDeps(observer: Owner): unknown[];
|
|
98
|
+
|
|
99
|
+
/** Opt-in native tracking-pass replay of pure synchronous accessors. */
|
|
100
|
+
export interface RetainedScope {
|
|
101
|
+
/**
|
|
102
|
+
* Register a pure synchronous read to execute during each future native
|
|
103
|
+
* tracking pass of the original computation. Does not execute immediately.
|
|
104
|
+
* Trigger a native recompute; returned function removes the registration.
|
|
105
|
+
* Invoke outside Solid-owned/tracking callbacks (e.g. after await or event).
|
|
106
|
+
*/
|
|
107
|
+
subscribe<T>(read: () => T): () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Must be called during EVERY tracking pass of the original computation.
|
|
112
|
+
* Unlike createScope(), it retains registered accessors and replays them
|
|
113
|
+
* in the original active observer; this is opt-in and does not mutate links.
|
|
114
|
+
* The registered callbacks must be pure, synchronous and safe to reexecute.
|
|
115
|
+
*/
|
|
116
|
+
export declare function createRetainedScope(): RetainedScope;
|