Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 405x 15x 15x 15x 15x 15x 15x 15x 15x 15x 4x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 17x 17x 17x 17x 17x 17x 17x 17x 2x 2x 1x 1x 2x 2x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 2x 2x 2x 2x 2x 15x 405x 405x 405x 405x 405x 405x 405x 405x 3x 3x 405x 405x 405x 405x 405x 405x 405x 405x 17x 4x 4x 17x 17x 17x 405x 405x 405x 405x 405x 405x 16x 6x 4x 4x 6x 4x 4x 6x 16x 405x 405x 405x 405x 405x 405x 405x 19x 19x 19x 19x 19x 19x 19x 19x 18x 18x 12x 2x 2x 12x 12x 10x 10x 10x 10x 10x 18x 18x 4x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 18x 19x 1x 1x 1x 1x 1x 1x 1x 19x 19x 19x 405x 405x 405x 405x 405x 405x 405x 405x 2x 2x 2x 405x 405x 405x 405x 405x 405x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 405x 405x 405x 405x 405x 405x 405x 405x 405x 5x 2x 2x 5x 1x 1x 2x 5x 405x 405x 405x 405x 405x 9x 9x 7x 7x 7x 9x 2x 2x 2x 9x 9x 9x 9x 405x | import { AvenxWatcher } from './watcher.js';
import { tracer } from '../trace/tracer.js';
import {
takeRecordedResponse,
traceResourceStart,
traceResourceSettle,
} from '../trace/resource.js';
/**
* A reactive resource that evaluates a handler function asynchronously.
* Supports Suspense and Error Boundaries by throwing Promises/Errors during render.
*/
export class Resource {
/**
* @param {string} name - Resource name.
* @param {function(AbortSignal): any} handlerFn - The function to execute (e.g., fetch call).
* @param {object} [componentContext] - The component instance context.
* @param {object} [options] - Resource options (e.g., pollInterval).
*/
constructor(name, handlerFn, componentContext, options = {}) {
this.name = name;
this.handlerFn = handlerFn;
let ctx = componentContext;
let opts = options;
if (
componentContext &&
typeof componentContext === 'object' &&
!('renderWatcher' in componentContext || 'update' in componentContext || '$app' in componentContext || 'state' in componentContext) &&
'pollInterval' in componentContext
) {
opts = componentContext;
ctx = null;
}
this.componentContext = ctx || null;
this.options = opts || {};
this.status = 'idle'; // 'idle' | 'pending' | 'resolved' | 'rejected'
this.value = undefined;
this.error = undefined;
this.promise = null;
// Race-condition guard and cancellation tokens
this._requestId = 0;
this._abortController = null;
this.pollTimer = null;
this.pollInterval = this.options.pollInterval ? Number(this.options.pollInterval) : 0;
/**
* The trace node for the request currently in flight, so its settlement can
* point back at it.
* @type {object|null}
*/
this.traceRequest = null;
// Create a watcher that tracks reactive dependencies inside handlerFn
this.watcher = new AvenxWatcher(
() => {
// During replay the recorded settlement stands in for the handler
// entirely, rather than running it and discarding the result: the
// handler *is* the network call, so calling it would hit the network.
// The cost is that a replayed resource does not re-track the
// dependencies its handler reads. Any behaviour that depends on it
// shows up as a divergence during replay rather than passing quietly.
const recorded = takeRecordedResponse(this.name);
if (recorded) {
return recorded.status === 'rejected'
? Promise.reject(Object.assign(new Error(recorded.error?.message || 'Recorded failure'), {
name: recorded.error?.name || 'Error',
}))
: Promise.resolve(recorded.value);
}
// Evaluate the handler to track dependencies
return this._executeHandler();
},
(newResult) => {
// Triggered when reactive dependencies change
this.fetch(newResult);
},
{ name: `Resource#${name}` }
);
// Initiate the first fetch manually with the initial evaluated value
this.fetch(this.watcher.value);
if (this.pollInterval > 0) {
this.pollTimer = setInterval(() => {
const val = typeof this.watcher.get === 'function' ? this.watcher.get() : this.watcher.value;
this.fetch(val);
}, this.pollInterval);
}
}
// --- Non-throwing Status Accessors ---
/**
* Indicates whether the resource is currently pending.
* @returns {boolean}
*/
get loading() {
return this.status === 'pending';
}
// --- Internal Helper Methods ---
/**
* Invokes the user handler passing the active abort signal.
* @private
*/
_executeHandler() {
if (this._abortController) {
this._abortController.abort();
}
this._abortController = new AbortController();
return this.handlerFn.call(this.componentContext, this._abortController.signal);
}
/**
* Notifies the parent component to schedule a re-render.
* @private
*/
_notifyComponent() {
if (this.componentContext) {
if (this.componentContext.renderWatcher) {
this.componentContext.renderWatcher.dirty = true;
}
if (typeof this.componentContext.update === 'function') {
this.componentContext.update();
}
}
}
/**
* Evaluates the resource result and updates internal state with request ID guard.
* @param {any} result - The result from the handler (Promise or sync value).
* @returns {Promise<any>}
*/
fetch(result) {
const currentId = ++this._requestId;
this.status = 'pending';
this.error = undefined;
const pending = tracer.on ? traceResourceStart(this.name, this.componentContext, this.pollInterval) : null;
this.traceRequest = pending;
if (result && typeof result.then === 'function') {
this.promise = result.then(
(val) => {
if (tracer.on) {
traceResourceSettle(pending, this.name, 'resolved', val);
}
// Ignore stale responses if a newer request was issued
if (currentId !== this._requestId) return val;
this.status = 'resolved';
this.value = val;
this._notifyComponent();
return val;
},
(err) => {
if (tracer.on) {
traceResourceSettle(pending, this.name, 'rejected', err);
}
// Ignore stale or intentionally aborted responses
if (currentId !== this._requestId) return;
if (err && (err.name === 'AbortError' || err.message === 'canceled')) return;
this.status = 'rejected';
this.error = err;
this._notifyComponent();
}
);
} else {
// Synchronous result handling
this.status = 'resolved';
this.value = result;
this.promise = Promise.resolve(result);
if (tracer.on) {
traceResourceSettle(pending, this.name, 'resolved', result);
}
this._notifyComponent();
}
return this.promise;
}
// --- Imperative API ---
/**
* Imperatively triggers a re-fetch, bypassing watcher dependency updates.
* @returns {Promise<any>}
*/
refetch() {
const result = this._executeHandler();
return this.fetch(result);
}
/**
* Imperatively sets the local value without a network call (useful for optimistic UI).
* @param {any} nextValue
*/
mutate(nextValue) {
// Invalidate any in-flight requests so a pending fetch doesn't override this mutation
this._requestId++;
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
}
this.status = 'resolved';
this.value = nextValue;
this.error = undefined;
this.promise = Promise.resolve(nextValue);
this._notifyComponent();
}
/**
* Reads the resource value.
* Throws Promise if pending (Suspense).
* Throws Error if rejected (ErrorBoundary).
* Returns value if resolved.
* @returns {any}
*/
read() {
if (this.status === 'pending' && this.promise) {
throw this.promise;
}
if (this.status === 'rejected') {
throw this.error;
}
return this.value;
}
/**
* Cleans up the watcher, aborts pending in-flight requests, and clears the polling timer.
*/
teardown() {
this._requestId++;
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
}
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
if (this.watcher) {
this.watcher.teardown();
}
}
} |