@kontsedal/olas-core 0.0.5 → 0.1.0
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/dist/index.cjs +0 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -27
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +109 -27
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +0 -0
- package/dist/index.mjs.map +1 -1
- package/dist/{root-DqWolle_.mjs → root-Byq-QYTp.mjs} +1478 -347
- package/dist/root-Byq-QYTp.mjs.map +1 -0
- package/dist/{root-lBp7qziQ.cjs → root-CPSL5kpR.cjs} +1483 -346
- package/dist/root-CPSL5kpR.cjs.map +1 -0
- package/dist/testing.cjs +5 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.cts.map +1 -1
- package/dist/testing.d.mts +3 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +5 -2
- package/dist/testing.mjs.map +1 -1
- package/dist/{types-BH1o6nYa.d.mts → types-Brp-4WaZ.d.mts} +244 -23
- package/dist/types-Brp-4WaZ.d.mts.map +1 -0
- package/dist/{types-C4Vtkxbn.d.cts → types-DzDIypPo.d.cts} +244 -23
- package/dist/types-DzDIypPo.d.cts.map +1 -0
- package/package.json +4 -1
- package/src/controller/instance.ts +360 -94
- package/src/controller/root.ts +58 -30
- package/src/controller/types.ts +66 -4
- package/src/emitter.ts +8 -2
- package/src/errors.ts +39 -3
- package/src/forms/field.ts +219 -25
- package/src/forms/form-types.ts +16 -3
- package/src/forms/form.ts +360 -38
- package/src/forms/index.ts +3 -1
- package/src/forms/types.ts +27 -1
- package/src/forms/validators.ts +45 -11
- package/src/index.ts +13 -7
- package/src/query/client.ts +237 -58
- package/src/query/define.ts +0 -0
- package/src/query/entry.ts +259 -15
- package/src/query/focus-online.ts +53 -11
- package/src/query/infinite.ts +351 -47
- package/src/query/keys.ts +33 -2
- package/src/query/local.ts +3 -0
- package/src/query/mutation.ts +27 -6
- package/src/query/plugin.ts +43 -4
- package/src/query/types.ts +76 -6
- package/src/query/use.ts +53 -10
- package/src/selection.ts +49 -12
- package/src/signals/readonly.ts +3 -0
- package/src/signals/runtime.ts +27 -0
- package/src/signals/types.ts +10 -0
- package/src/testing.ts +9 -0
- package/src/timing/debounced.ts +106 -23
- package/src/timing/index.ts +1 -1
- package/src/timing/throttled.ts +85 -28
- package/src/utils.ts +15 -2
- package/dist/root-DqWolle_.mjs.map +0 -1
- package/dist/root-lBp7qziQ.cjs.map +0 -1
- package/dist/types-BH1o6nYa.d.mts.map +0 -1
- package/dist/types-C4Vtkxbn.d.cts.map +0 -1
|
@@ -103,6 +103,11 @@ var DevtoolsEmitter = class {
|
|
|
103
103
|
const defaultHandler = (err, context) => {
|
|
104
104
|
console.error("[olas]", context, err);
|
|
105
105
|
};
|
|
106
|
+
let eventCounter = 0;
|
|
107
|
+
function nextEventId() {
|
|
108
|
+
eventCounter = eventCounter + 1 >>> 0;
|
|
109
|
+
return `e_${(Math.random() * 16777216 | 0).toString(16).padStart(6, "0")}${eventCounter.toString(16).padStart(6, "0")}`;
|
|
110
|
+
}
|
|
106
111
|
/**
|
|
107
112
|
* Dispatch an error to a user-provided handler, falling back to console.error.
|
|
108
113
|
* The handler itself is wrapped — if it throws, the throw is swallowed and
|
|
@@ -112,16 +117,46 @@ const defaultHandler = (err, context) => {
|
|
|
112
117
|
*/
|
|
113
118
|
function dispatchError(handler, err, context) {
|
|
114
119
|
const fn = handler ?? defaultHandler;
|
|
120
|
+
const full = {
|
|
121
|
+
...context,
|
|
122
|
+
eventId: nextEventId(),
|
|
123
|
+
timestamp: Date.now()
|
|
124
|
+
};
|
|
115
125
|
try {
|
|
116
|
-
fn(err,
|
|
126
|
+
fn(err, full);
|
|
117
127
|
} catch (handlerErr) {
|
|
118
128
|
try {
|
|
119
129
|
console.error("[olas] onError handler threw:", handlerErr);
|
|
120
|
-
console.error("[olas] original error:", err,
|
|
130
|
+
console.error("[olas] original error:", err, full);
|
|
121
131
|
} catch {}
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
//#endregion
|
|
135
|
+
//#region src/signals/readonly.ts
|
|
136
|
+
/**
|
|
137
|
+
* Project a Signal (or any object with a reactive `value` + `peek` + `subscribe`)
|
|
138
|
+
* as a `ReadSignal`. The returned object does not expose `set` / `update` /
|
|
139
|
+
* settable `value`, so it can be returned from APIs without callers mutating it.
|
|
140
|
+
*
|
|
141
|
+
* Internal helper — not exported from the package's public surface.
|
|
142
|
+
*/
|
|
143
|
+
function readOnly(source) {
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
get value() {
|
|
146
|
+
return source.value;
|
|
147
|
+
},
|
|
148
|
+
peek() {
|
|
149
|
+
return source.peek();
|
|
150
|
+
},
|
|
151
|
+
subscribe(handler) {
|
|
152
|
+
return source.subscribe(handler);
|
|
153
|
+
},
|
|
154
|
+
subscribeChanges(handler) {
|
|
155
|
+
return source.subscribeChanges(handler);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
125
160
|
//#region src/signals/runtime.ts
|
|
126
161
|
var SignalImpl = class {
|
|
127
162
|
inner;
|
|
@@ -140,6 +175,9 @@ var SignalImpl = class {
|
|
|
140
175
|
subscribe(handler) {
|
|
141
176
|
return this.inner.subscribe(handler);
|
|
142
177
|
}
|
|
178
|
+
subscribeChanges(handler) {
|
|
179
|
+
return subscribeChangesImpl(this.inner, handler);
|
|
180
|
+
}
|
|
143
181
|
set(value) {
|
|
144
182
|
this.inner.value = value;
|
|
145
183
|
}
|
|
@@ -161,8 +199,26 @@ var ComputedImpl = class {
|
|
|
161
199
|
subscribe(handler) {
|
|
162
200
|
return this.inner.subscribe(handler);
|
|
163
201
|
}
|
|
202
|
+
subscribeChanges(handler) {
|
|
203
|
+
return subscribeChangesImpl(this.inner, handler);
|
|
204
|
+
}
|
|
164
205
|
};
|
|
165
206
|
/**
|
|
207
|
+
* Shared skip-initial wrapper around `signal.subscribe`. Preact fires the
|
|
208
|
+
* subscribe callback synchronously with the current value (the "initial
|
|
209
|
+
* fire"); the wrapper swallows it and forwards every subsequent change.
|
|
210
|
+
*/
|
|
211
|
+
function subscribeChangesImpl(inner, handler) {
|
|
212
|
+
let initial = true;
|
|
213
|
+
return inner.subscribe((value) => {
|
|
214
|
+
if (initial) {
|
|
215
|
+
initial = false;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
handler(value);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
166
222
|
* Create a writable `Signal<T>`. Reads track the current auto-tracking scope
|
|
167
223
|
* (effect / computed); writes notify all subscribers (deduped via `Object.is`).
|
|
168
224
|
*
|
|
@@ -232,7 +288,7 @@ function isAbortError(err) {
|
|
|
232
288
|
function abortableSleep(ms, signal) {
|
|
233
289
|
return new Promise((resolve, reject) => {
|
|
234
290
|
if (signal.aborted) {
|
|
235
|
-
reject(
|
|
291
|
+
reject(abortReason(signal));
|
|
236
292
|
return;
|
|
237
293
|
}
|
|
238
294
|
const timer = setTimeout(() => {
|
|
@@ -242,11 +298,93 @@ function abortableSleep(ms, signal) {
|
|
|
242
298
|
const onAbort = () => {
|
|
243
299
|
clearTimeout(timer);
|
|
244
300
|
signal.removeEventListener("abort", onAbort);
|
|
245
|
-
reject(
|
|
301
|
+
reject(abortReason(signal));
|
|
246
302
|
};
|
|
247
303
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
248
304
|
});
|
|
249
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Read the caller-supplied abort reason if present, falling back to the
|
|
308
|
+
* canonical `DOMException('Aborted', 'AbortError')`. `signal.reason` lets
|
|
309
|
+
* users propagate context (e.g. "supersededByLatestWins") through abort
|
|
310
|
+
* chains — discarding it would force every caller to wrap with custom
|
|
311
|
+
* cancellation tokens.
|
|
312
|
+
*/
|
|
313
|
+
function abortReason(signal) {
|
|
314
|
+
const reason = signal.reason;
|
|
315
|
+
if (reason !== void 0) return reason;
|
|
316
|
+
return new DOMException("Aborted", "AbortError");
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/query/focus-online.ts
|
|
320
|
+
const focusSubs = /* @__PURE__ */ new Set();
|
|
321
|
+
const onlineSubs = /* @__PURE__ */ new Set();
|
|
322
|
+
function fireFocus() {
|
|
323
|
+
for (const fn of focusSubs) try {
|
|
324
|
+
fn();
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
function fireOnline() {
|
|
328
|
+
for (const fn of onlineSubs) try {
|
|
329
|
+
fn();
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
let focusScheduled = false;
|
|
333
|
+
function scheduleFocus() {
|
|
334
|
+
if (focusScheduled) return;
|
|
335
|
+
focusScheduled = true;
|
|
336
|
+
queueMicrotask(() => {
|
|
337
|
+
focusScheduled = false;
|
|
338
|
+
fireFocus();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function onVisibilityChange() {
|
|
342
|
+
if (document.visibilityState === "visible") scheduleFocus();
|
|
343
|
+
}
|
|
344
|
+
let focusInstalled = false;
|
|
345
|
+
let onlineInstalled = false;
|
|
346
|
+
function installFocus() {
|
|
347
|
+
if (focusInstalled) return;
|
|
348
|
+
if (typeof window === "undefined") return;
|
|
349
|
+
window.addEventListener("focus", scheduleFocus);
|
|
350
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibilityChange);
|
|
351
|
+
focusInstalled = true;
|
|
352
|
+
}
|
|
353
|
+
function uninstallFocus() {
|
|
354
|
+
if (!focusInstalled) return;
|
|
355
|
+
if (typeof window === "undefined") return;
|
|
356
|
+
window.removeEventListener("focus", scheduleFocus);
|
|
357
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
358
|
+
focusInstalled = false;
|
|
359
|
+
}
|
|
360
|
+
function installOnline() {
|
|
361
|
+
if (onlineInstalled) return;
|
|
362
|
+
if (typeof window === "undefined") return;
|
|
363
|
+
window.addEventListener("online", fireOnline);
|
|
364
|
+
onlineInstalled = true;
|
|
365
|
+
}
|
|
366
|
+
function uninstallOnline() {
|
|
367
|
+
if (!onlineInstalled) return;
|
|
368
|
+
if (typeof window === "undefined") return;
|
|
369
|
+
window.removeEventListener("online", fireOnline);
|
|
370
|
+
onlineInstalled = false;
|
|
371
|
+
}
|
|
372
|
+
function subscribeWindowFocus(fn) {
|
|
373
|
+
installFocus();
|
|
374
|
+
focusSubs.add(fn);
|
|
375
|
+
return () => {
|
|
376
|
+
focusSubs.delete(fn);
|
|
377
|
+
if (focusSubs.size === 0) uninstallFocus();
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function subscribeReconnect(fn) {
|
|
381
|
+
installOnline();
|
|
382
|
+
onlineSubs.add(fn);
|
|
383
|
+
return () => {
|
|
384
|
+
onlineSubs.delete(fn);
|
|
385
|
+
if (onlineSubs.size === 0) uninstallOnline();
|
|
386
|
+
};
|
|
387
|
+
}
|
|
250
388
|
//#endregion
|
|
251
389
|
//#region src/query/structural-share.ts
|
|
252
390
|
/**
|
|
@@ -341,16 +479,33 @@ var Entry = class {
|
|
|
341
479
|
lastUpdatedAt;
|
|
342
480
|
hasPendingMutations = signal(false);
|
|
343
481
|
isStale = signal(true);
|
|
482
|
+
/** True while a fetch is parked waiting for reconnect (online-mode defer or
|
|
483
|
+
* offlineFirst network-error park). See spec §5.5, T3.5. */
|
|
484
|
+
isPaused = signal(false);
|
|
344
485
|
fetcherProvider;
|
|
345
486
|
staleTime;
|
|
346
487
|
retry;
|
|
347
488
|
retryDelay;
|
|
489
|
+
networkMode;
|
|
490
|
+
structuralShareEnabled;
|
|
348
491
|
currentFetchId = 0;
|
|
349
492
|
currentAbort = null;
|
|
350
493
|
staleTimer = null;
|
|
494
|
+
/** Set by `markStale()` (invalidate without fetch); forces `isStaleNow()`
|
|
495
|
+
* true until the next successful fetch clears it. Spec §5.7, T3.9. */
|
|
496
|
+
forcedStale = false;
|
|
351
497
|
snapshots = [];
|
|
352
498
|
nextSnapshotId = 0;
|
|
353
499
|
disposed = false;
|
|
500
|
+
/** Subscribers to reconnect — installed lazily when a deferred fetch lands. */
|
|
501
|
+
reconnectUnsub = null;
|
|
502
|
+
/**
|
|
503
|
+
* Set of deferred-fetch resolvers waiting for reconnect (online mode).
|
|
504
|
+
* Stored at `unknown` to keep `Entry<T>` covariant in `T` — same trick as
|
|
505
|
+
* `onSuccessData`. Each resolver is fed the same value from
|
|
506
|
+
* `applySuccess`, which is `T`, then cast at the fan-out call site.
|
|
507
|
+
*/
|
|
508
|
+
deferredResolvers = [];
|
|
354
509
|
events;
|
|
355
510
|
onSuccessData;
|
|
356
511
|
fetchStartTime = 0;
|
|
@@ -364,7 +519,9 @@ var Entry = class {
|
|
|
364
519
|
this.fetcherProvider = options.fetcher;
|
|
365
520
|
this.staleTime = options.staleTime ?? 0;
|
|
366
521
|
this.retry = options.retry ?? 0;
|
|
367
|
-
this.retryDelay = options.retryDelay
|
|
522
|
+
this.retryDelay = options.retryDelay;
|
|
523
|
+
this.networkMode = options.networkMode ?? "online";
|
|
524
|
+
this.structuralShareEnabled = options.structuralShare ?? true;
|
|
368
525
|
this.events = options.events ?? {};
|
|
369
526
|
this.onSuccessData = options.onSuccessData;
|
|
370
527
|
this.data = signal(options.initialData);
|
|
@@ -388,6 +545,7 @@ var Entry = class {
|
|
|
388
545
|
}
|
|
389
546
|
startFetch() {
|
|
390
547
|
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Entry disposed"));
|
|
548
|
+
if (this.networkMode === "online" && this.isOffline()) return this.scheduleDeferredFetch();
|
|
391
549
|
const myId = ++this.currentFetchId;
|
|
392
550
|
this.currentAbort?.abort();
|
|
393
551
|
const abort = new AbortController();
|
|
@@ -397,6 +555,7 @@ var Entry = class {
|
|
|
397
555
|
this.status.set("pending");
|
|
398
556
|
this.isFetching.set(true);
|
|
399
557
|
this.isLoading.set(!previouslyHadData);
|
|
558
|
+
this.isPaused.set(false);
|
|
400
559
|
});
|
|
401
560
|
this.fetchStartTime = Date.now();
|
|
402
561
|
try {
|
|
@@ -404,6 +563,34 @@ var Entry = class {
|
|
|
404
563
|
} catch {}
|
|
405
564
|
return this.runWithRetry(myId, abort);
|
|
406
565
|
}
|
|
566
|
+
isOffline() {
|
|
567
|
+
return typeof navigator !== "undefined" && navigator.onLine === false;
|
|
568
|
+
}
|
|
569
|
+
scheduleDeferredFetch() {
|
|
570
|
+
this.isPaused.set(true);
|
|
571
|
+
if (this.reconnectUnsub === null) this.reconnectUnsub = subscribeReconnect(() => this.drainDeferred());
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
this.deferredResolvers.push({
|
|
574
|
+
resolve,
|
|
575
|
+
reject
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
drainDeferred() {
|
|
580
|
+
if (this.deferredResolvers.length === 0) return;
|
|
581
|
+
if (this.disposed) return;
|
|
582
|
+
const pending = this.deferredResolvers;
|
|
583
|
+
this.deferredResolvers = [];
|
|
584
|
+
if (this.reconnectUnsub !== null) {
|
|
585
|
+
this.reconnectUnsub();
|
|
586
|
+
this.reconnectUnsub = null;
|
|
587
|
+
}
|
|
588
|
+
this.startFetch().then((value) => {
|
|
589
|
+
for (const p of pending) p.resolve(value);
|
|
590
|
+
}, (err) => {
|
|
591
|
+
for (const p of pending) p.reject(err);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
407
594
|
async runWithRetry(myId, abort) {
|
|
408
595
|
let attempt = 0;
|
|
409
596
|
while (true) {
|
|
@@ -414,6 +601,14 @@ var Entry = class {
|
|
|
414
601
|
return this.applySuccess(result);
|
|
415
602
|
} catch (err) {
|
|
416
603
|
if (myId !== this.currentFetchId || this.disposed || isAbortError(err)) throw err;
|
|
604
|
+
if (this.networkMode === "offlineFirst" && this.isOffline() && err instanceof TypeError) {
|
|
605
|
+
batch(() => {
|
|
606
|
+
this.isFetching.set(false);
|
|
607
|
+
this.isLoading.set(false);
|
|
608
|
+
this.status.set(this.data.peek() !== void 0 ? "success" : "idle");
|
|
609
|
+
});
|
|
610
|
+
return this.scheduleDeferredFetch();
|
|
611
|
+
}
|
|
417
612
|
if (!this.shouldRetry(attempt, err)) return this.applyFailure(err);
|
|
418
613
|
await abortableSleep(this.computeDelay(attempt), abort.signal);
|
|
419
614
|
attempt += 1;
|
|
@@ -428,11 +623,13 @@ var Entry = class {
|
|
|
428
623
|
}
|
|
429
624
|
computeDelay(attempt) {
|
|
430
625
|
const d = this.retryDelay;
|
|
626
|
+
if (d === void 0) return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
431
627
|
return typeof d === "function" ? d(attempt) : d;
|
|
432
628
|
}
|
|
433
629
|
applySuccess(result) {
|
|
434
630
|
const prev = this.data.peek();
|
|
435
|
-
const shared = prev === void 0 ? result : structuralShare(prev, result);
|
|
631
|
+
const shared = prev === void 0 || !this.structuralShareEnabled ? result : structuralShare(prev, result);
|
|
632
|
+
if (this.snapshots.length > 0) for (const s of this.snapshots) s.prev = shared;
|
|
436
633
|
batch(() => {
|
|
437
634
|
this.data.set(shared);
|
|
438
635
|
this.error.set(void 0);
|
|
@@ -442,6 +639,7 @@ var Entry = class {
|
|
|
442
639
|
this.lastUpdatedAt.set(Date.now());
|
|
443
640
|
this.isStale.set(this.staleTime === 0);
|
|
444
641
|
});
|
|
642
|
+
this.forcedStale = false;
|
|
445
643
|
if (this.staleTime > 0) this.scheduleStaleness();
|
|
446
644
|
try {
|
|
447
645
|
this.events.onFetchSuccess?.(Date.now() - this.fetchStartTime);
|
|
@@ -471,12 +669,66 @@ var Entry = class {
|
|
|
471
669
|
refetch() {
|
|
472
670
|
return this.startFetch();
|
|
473
671
|
}
|
|
474
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Apply a server-supplied data + timestamp without going through the
|
|
674
|
+
* fetcher path. Used by streaming SSR hydration: each `<Suspense>` boundary
|
|
675
|
+
* that resolves on the server pushes its entry's data to the client, and
|
|
676
|
+
* the client routes it through here so the entry transitions to `success`
|
|
677
|
+
* without burning the user's fetcher.
|
|
678
|
+
*
|
|
679
|
+
* Distinct from `setData`: no Snapshot returned (no rollback semantics —
|
|
680
|
+
* this is canonical data, not an optimistic patch), no
|
|
681
|
+
* `hasPendingMutations` flip, and `lastUpdatedAt` honors the supplied
|
|
682
|
+
* server timestamp instead of `Date.now()`. Also bumps `currentFetchId`
|
|
683
|
+
* so any in-flight fetch supersedes itself rather than overwriting the
|
|
684
|
+
* fresher hydrated value.
|
|
685
|
+
*/
|
|
686
|
+
applyHydration(data, lastUpdatedAt) {
|
|
687
|
+
if (this.disposed) return;
|
|
688
|
+
this.currentFetchId += 1;
|
|
689
|
+
this.currentAbort?.abort();
|
|
690
|
+
this.currentAbort = null;
|
|
691
|
+
if (this.staleTimer !== null) {
|
|
692
|
+
clearTimeout(this.staleTimer);
|
|
693
|
+
this.staleTimer = null;
|
|
694
|
+
}
|
|
695
|
+
const alreadyStale = this.staleTime === 0 || Date.now() - lastUpdatedAt >= this.staleTime;
|
|
696
|
+
batch(() => {
|
|
697
|
+
this.data.set(data);
|
|
698
|
+
this.error.set(void 0);
|
|
699
|
+
this.status.set("success");
|
|
700
|
+
this.isLoading.set(false);
|
|
701
|
+
this.isFetching.set(false);
|
|
702
|
+
this.lastUpdatedAt.set(lastUpdatedAt);
|
|
703
|
+
this.isStale.set(alreadyStale);
|
|
704
|
+
});
|
|
705
|
+
if (!alreadyStale && this.staleTime > 0) {
|
|
706
|
+
const remaining = this.staleTime - (Date.now() - lastUpdatedAt);
|
|
707
|
+
this.staleTimer = setTimeout(() => {
|
|
708
|
+
this.staleTimer = null;
|
|
709
|
+
if (!this.disposed) this.isStale.set(true);
|
|
710
|
+
}, remaining);
|
|
711
|
+
}
|
|
712
|
+
this.onSuccessData?.(data);
|
|
713
|
+
if (this.pendingFirstValueRejects.length > 0) {}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Force this entry stale WITHOUT fetching. `isStaleNow()` returns true until
|
|
717
|
+
* the next successful fetch clears the flag, so the next subscriber refetches.
|
|
718
|
+
* Used by `client.invalidate` for subscriber-less entries — spec §5.7 says
|
|
719
|
+
* invalidate refetches only IF subscribed (T3.9).
|
|
720
|
+
*/
|
|
721
|
+
markStale() {
|
|
722
|
+
if (this.disposed) return;
|
|
475
723
|
if (this.staleTimer != null) {
|
|
476
724
|
clearTimeout(this.staleTimer);
|
|
477
725
|
this.staleTimer = null;
|
|
478
726
|
}
|
|
727
|
+
this.forcedStale = true;
|
|
479
728
|
this.isStale.set(true);
|
|
729
|
+
}
|
|
730
|
+
invalidate() {
|
|
731
|
+
this.markStale();
|
|
480
732
|
return this.startFetch();
|
|
481
733
|
}
|
|
482
734
|
reset() {
|
|
@@ -486,35 +738,80 @@ var Entry = class {
|
|
|
486
738
|
this.status.set(this.data.peek() !== void 0 ? "success" : "idle");
|
|
487
739
|
});
|
|
488
740
|
}
|
|
489
|
-
|
|
741
|
+
/**
|
|
742
|
+
* Cancel an in-flight fetch without touching `data`. Aborts the current
|
|
743
|
+
* request and supersedes it (bumps `currentFetchId` so its result can never
|
|
744
|
+
* land), then restores a settled status: `'success'` if data exists, else
|
|
745
|
+
* `'idle'`. No-op when nothing is fetching. This is the primitive behind
|
|
746
|
+
* `query.cancel(...)` / `subscription.cancel()`: the canonical optimistic
|
|
747
|
+
* recipe cancels outgoing refetches before an optimistic `setData` so a
|
|
748
|
+
* stale response can't clobber the optimistic value (spec §5, §6.4). T3.4.
|
|
749
|
+
*/
|
|
750
|
+
cancel() {
|
|
751
|
+
if (this.disposed || !this.isFetching.peek()) return;
|
|
752
|
+
this.currentFetchId += 1;
|
|
753
|
+
this.currentAbort?.abort();
|
|
754
|
+
this.currentAbort = null;
|
|
755
|
+
batch(() => {
|
|
756
|
+
this.isFetching.set(false);
|
|
757
|
+
this.isLoading.set(false);
|
|
758
|
+
this.status.set(this.data.peek() !== void 0 ? "success" : "idle");
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Write data into the entry.
|
|
763
|
+
*
|
|
764
|
+
* `track` (default `true`) is the optimistic-update path: it pushes a
|
|
765
|
+
* snapshot record and flips `hasPendingMutations`, so the returned
|
|
766
|
+
* `Snapshot.rollback()` can restore the pre-write baseline and
|
|
767
|
+
* `finalize()` commits it. This is what `query.setData(...)` inside a
|
|
768
|
+
* mutation's `onMutate` uses (spec §6.4).
|
|
769
|
+
*
|
|
770
|
+
* `track: false` is a canonical cache write — cross-tab receive, entities
|
|
771
|
+
* backprop, realtime patches (spec §13.2). It updates the data signal but
|
|
772
|
+
* pushes NO snapshot and does NOT flip `hasPendingMutations`, so a
|
|
773
|
+
* fire-and-forget plugin write can't wedge the pending flag at `true`
|
|
774
|
+
* forever. Returns a no-op `Snapshot`.
|
|
775
|
+
*/
|
|
776
|
+
setData(updater, opts) {
|
|
490
777
|
if (this.disposed) return {
|
|
491
778
|
rollback: () => {},
|
|
492
779
|
finalize: () => {}
|
|
493
780
|
};
|
|
494
781
|
const prev = this.data.peek();
|
|
495
782
|
const next = updater(prev);
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
id,
|
|
783
|
+
const record = opts?.track ?? true ? {
|
|
784
|
+
id: this.nextSnapshotId++,
|
|
499
785
|
prev,
|
|
500
786
|
live: true
|
|
501
|
-
};
|
|
502
|
-
this.snapshots.push(record);
|
|
787
|
+
} : null;
|
|
788
|
+
if (record) this.snapshots.push(record);
|
|
503
789
|
batch(() => {
|
|
504
790
|
this.data.set(next);
|
|
505
791
|
if (this.status.peek() === "idle" || this.status.peek() === "pending") this.status.set("success");
|
|
506
792
|
this.lastUpdatedAt.set(Date.now());
|
|
507
|
-
this.hasPendingMutations.set(true);
|
|
793
|
+
if (record) this.hasPendingMutations.set(true);
|
|
508
794
|
});
|
|
795
|
+
if (!record) return {
|
|
796
|
+
rollback: () => {},
|
|
797
|
+
finalize: () => {}
|
|
798
|
+
};
|
|
799
|
+
const id = record.id;
|
|
509
800
|
return {
|
|
510
801
|
rollback: () => {
|
|
511
802
|
if (!record.live || this.disposed) return;
|
|
512
803
|
record.live = false;
|
|
513
804
|
batch(() => {
|
|
514
|
-
this.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
805
|
+
const i = this.snapshots.indexOf(record);
|
|
806
|
+
if (i !== -1) {
|
|
807
|
+
if (i === this.snapshots.length - 1) this.data.set(record.prev);
|
|
808
|
+
else {
|
|
809
|
+
const below = this.snapshots[i + 1];
|
|
810
|
+
below.prev = record.prev;
|
|
811
|
+
}
|
|
812
|
+
this.snapshots.splice(i, 1);
|
|
813
|
+
}
|
|
814
|
+
this.hasPendingMutations.set(this.snapshots.some((s) => s.live));
|
|
518
815
|
});
|
|
519
816
|
},
|
|
520
817
|
finalize: () => {
|
|
@@ -552,6 +849,7 @@ var Entry = class {
|
|
|
552
849
|
* Used by the query client to decide whether to refetch on subscribe.
|
|
553
850
|
*/
|
|
554
851
|
isStaleNow() {
|
|
852
|
+
if (this.forcedStale) return true;
|
|
555
853
|
const last = this.lastUpdatedAt.peek();
|
|
556
854
|
if (last === void 0) return true;
|
|
557
855
|
return Date.now() - last >= this.staleTime;
|
|
@@ -565,6 +863,20 @@ var Entry = class {
|
|
|
565
863
|
}
|
|
566
864
|
this.currentAbort?.abort();
|
|
567
865
|
this.currentAbort = null;
|
|
866
|
+
batch(() => {
|
|
867
|
+
this.isFetching.set(false);
|
|
868
|
+
this.isLoading.set(false);
|
|
869
|
+
});
|
|
870
|
+
if (this.reconnectUnsub !== null) {
|
|
871
|
+
this.reconnectUnsub();
|
|
872
|
+
this.reconnectUnsub = null;
|
|
873
|
+
}
|
|
874
|
+
if (this.deferredResolvers.length > 0) {
|
|
875
|
+
const disposed = new DOMException("Entry disposed", "AbortError");
|
|
876
|
+
const pending = this.deferredResolvers;
|
|
877
|
+
this.deferredResolvers = [];
|
|
878
|
+
for (const p of pending) p.reject(disposed);
|
|
879
|
+
}
|
|
568
880
|
if (this.pendingFirstValueRejects.length > 0) {
|
|
569
881
|
const disposed = new DOMException("Entry disposed", "AbortError");
|
|
570
882
|
const rejects = this.pendingFirstValueRejects;
|
|
@@ -574,51 +886,6 @@ var Entry = class {
|
|
|
574
886
|
}
|
|
575
887
|
};
|
|
576
888
|
//#endregion
|
|
577
|
-
//#region src/query/focus-online.ts
|
|
578
|
-
const focusSubs = /* @__PURE__ */ new Set();
|
|
579
|
-
const onlineSubs = /* @__PURE__ */ new Set();
|
|
580
|
-
let focusInstalled = false;
|
|
581
|
-
let onlineInstalled = false;
|
|
582
|
-
function fireFocus() {
|
|
583
|
-
for (const fn of focusSubs) try {
|
|
584
|
-
fn();
|
|
585
|
-
} catch {}
|
|
586
|
-
}
|
|
587
|
-
function fireOnline() {
|
|
588
|
-
for (const fn of onlineSubs) try {
|
|
589
|
-
fn();
|
|
590
|
-
} catch {}
|
|
591
|
-
}
|
|
592
|
-
function ensureFocusInstalled() {
|
|
593
|
-
if (focusInstalled) return;
|
|
594
|
-
if (typeof window === "undefined") return;
|
|
595
|
-
window.addEventListener("focus", fireFocus);
|
|
596
|
-
if (typeof document !== "undefined") document.addEventListener("visibilitychange", () => {
|
|
597
|
-
if (document.visibilityState === "visible") fireFocus();
|
|
598
|
-
});
|
|
599
|
-
focusInstalled = true;
|
|
600
|
-
}
|
|
601
|
-
function ensureOnlineInstalled() {
|
|
602
|
-
if (onlineInstalled) return;
|
|
603
|
-
if (typeof window === "undefined") return;
|
|
604
|
-
window.addEventListener("online", fireOnline);
|
|
605
|
-
onlineInstalled = true;
|
|
606
|
-
}
|
|
607
|
-
function subscribeWindowFocus(fn) {
|
|
608
|
-
ensureFocusInstalled();
|
|
609
|
-
focusSubs.add(fn);
|
|
610
|
-
return () => {
|
|
611
|
-
focusSubs.delete(fn);
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
function subscribeReconnect(fn) {
|
|
615
|
-
ensureOnlineInstalled();
|
|
616
|
-
onlineSubs.add(fn);
|
|
617
|
-
return () => {
|
|
618
|
-
onlineSubs.delete(fn);
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
//#endregion
|
|
622
889
|
//#region src/query/infinite.ts
|
|
623
890
|
/**
|
|
624
891
|
* Holds an array of pages plus their pageParams. Supports fetchNextPage /
|
|
@@ -637,6 +904,9 @@ var InfiniteEntry = class {
|
|
|
637
904
|
isStale = signal(true);
|
|
638
905
|
lastUpdatedAt = signal(void 0);
|
|
639
906
|
hasPendingMutations = signal(false);
|
|
907
|
+
/** True while a fetch is parked waiting for reconnect (online-mode defer).
|
|
908
|
+
* See spec §5.5, T3.5. Mirrors `Entry.isPaused`. */
|
|
909
|
+
isPaused = signal(false);
|
|
640
910
|
isFetchingNextPage = signal(false);
|
|
641
911
|
isFetchingPreviousPage = signal(false);
|
|
642
912
|
hasNextPage;
|
|
@@ -645,6 +915,8 @@ var InfiniteEntry = class {
|
|
|
645
915
|
currentFetchId = 0;
|
|
646
916
|
currentAbort = null;
|
|
647
917
|
staleTimer = null;
|
|
918
|
+
/** Set by `markStale()` (invalidate without fetch). See `Entry.forcedStale`. */
|
|
919
|
+
forcedStale = false;
|
|
648
920
|
snapshots = [];
|
|
649
921
|
nextSnapshotId = 0;
|
|
650
922
|
disposed = false;
|
|
@@ -657,6 +929,10 @@ var InfiniteEntry = class {
|
|
|
657
929
|
staleTime;
|
|
658
930
|
retry;
|
|
659
931
|
retryDelay;
|
|
932
|
+
networkMode;
|
|
933
|
+
structuralShareEnabled;
|
|
934
|
+
reconnectUnsub = null;
|
|
935
|
+
deferredResolvers = [];
|
|
660
936
|
itemsOf;
|
|
661
937
|
/**
|
|
662
938
|
* Mirrors `Entry.onSuccessData`. Fires from `applyFetchSuccess`-equivalent
|
|
@@ -673,7 +949,9 @@ var InfiniteEntry = class {
|
|
|
673
949
|
this.itemsOf = opts.itemsOf;
|
|
674
950
|
this.staleTime = opts.staleTime ?? 0;
|
|
675
951
|
this.retry = opts.retry ?? 0;
|
|
676
|
-
this.retryDelay = opts.retryDelay
|
|
952
|
+
this.retryDelay = opts.retryDelay;
|
|
953
|
+
this.networkMode = opts.networkMode ?? "online";
|
|
954
|
+
this.structuralShareEnabled = opts.structuralShare ?? true;
|
|
677
955
|
this.onSuccessData = opts.onSuccessData;
|
|
678
956
|
this.pageParams = signal([]);
|
|
679
957
|
this.data = computed(() => {
|
|
@@ -700,26 +978,82 @@ var InfiniteEntry = class {
|
|
|
700
978
|
return fn(ps[0], ps) !== null;
|
|
701
979
|
});
|
|
702
980
|
}
|
|
703
|
-
/**
|
|
981
|
+
/**
|
|
982
|
+
* Initial load / refetch. On the initial load (no pages yet) this fetches
|
|
983
|
+
* the first page. On a refetch of an already-loaded entry (interval,
|
|
984
|
+
* invalidate, focus/reconnect) it **refetches every currently-loaded page**
|
|
985
|
+
* sequentially, re-deriving each page's param from the freshly-fetched data
|
|
986
|
+
* via `getNextPageParam` — matching TanStack and avoiding the old
|
|
987
|
+
* "collapse to page one" truncation that dropped every loaded page but the
|
|
988
|
+
* first on each refetch (T3.7). Pages/params update atomically at the end,
|
|
989
|
+
* so subscribers never observe a mid-refetch truncation flash.
|
|
990
|
+
*/
|
|
704
991
|
startFetch() {
|
|
705
992
|
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Entry disposed"));
|
|
993
|
+
if (this.networkMode === "online" && this.isOffline()) return this.scheduleDeferredFetch("initial");
|
|
706
994
|
const myId = ++this.currentFetchId;
|
|
707
995
|
this.currentAbort?.abort();
|
|
708
996
|
const abort = new AbortController();
|
|
709
997
|
this.currentAbort = abort;
|
|
710
|
-
const
|
|
998
|
+
const previousPages = this.pages.peek();
|
|
711
999
|
batch(() => {
|
|
712
1000
|
this.status.set("pending");
|
|
713
1001
|
this.isFetching.set(true);
|
|
714
|
-
this.isLoading.set(
|
|
1002
|
+
this.isLoading.set(previousPages.length === 0);
|
|
1003
|
+
this.isPaused.set(false);
|
|
715
1004
|
});
|
|
716
|
-
return this.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1005
|
+
return this.runRefetchAll(myId, abort.signal, Math.max(1, previousPages.length), previousPages);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Fetch `targetCount` pages from `initialPageParam`, chaining each next param
|
|
1009
|
+
* via `getNextPageParam` from the freshly-fetched pages. Applies the query's
|
|
1010
|
+
* retry policy per page. Stops early if the dataset shrank (a page yields
|
|
1011
|
+
* `getNextPageParam === null`). Writes pages/params in ONE batch at the end;
|
|
1012
|
+
* a per-page failure keeps the existing pages and surfaces the error; a
|
|
1013
|
+
* supersede/dispose throws `AbortError` without writing.
|
|
1014
|
+
*/
|
|
1015
|
+
async runRefetchAll(myId, signal, targetCount, previousPages) {
|
|
1016
|
+
const newPages = [];
|
|
1017
|
+
const newParams = [];
|
|
1018
|
+
let pageParam = this.initialPageParam;
|
|
1019
|
+
try {
|
|
1020
|
+
for (let i = 0; i < targetCount; i++) {
|
|
1021
|
+
let attempt = 0;
|
|
1022
|
+
while (true) {
|
|
1023
|
+
if (myId !== this.currentFetchId || this.disposed) throw new DOMException("Superseded", "AbortError");
|
|
1024
|
+
try {
|
|
1025
|
+
const page = await this.fetcher({
|
|
1026
|
+
pageParam,
|
|
1027
|
+
signal
|
|
1028
|
+
});
|
|
1029
|
+
if (myId !== this.currentFetchId || this.disposed) throw new DOMException("Superseded", "AbortError");
|
|
1030
|
+
newPages.push(page);
|
|
1031
|
+
newParams.push(pageParam);
|
|
1032
|
+
break;
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
if (myId !== this.currentFetchId || this.disposed || isAbortError(err)) throw err;
|
|
1035
|
+
if (!(typeof this.retry === "number" ? attempt < this.retry : this.retry(attempt, err))) {
|
|
1036
|
+
batch(() => {
|
|
1037
|
+
this.error.set(err);
|
|
1038
|
+
this.status.set("error");
|
|
1039
|
+
this.isLoading.set(false);
|
|
1040
|
+
this.isFetching.set(false);
|
|
1041
|
+
});
|
|
1042
|
+
throw err;
|
|
1043
|
+
}
|
|
1044
|
+
await abortableSleep(this.computeRetryDelay(attempt), signal);
|
|
1045
|
+
attempt += 1;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const next = this.getNextPageParam(newPages[newPages.length - 1], newPages);
|
|
1049
|
+
if (next === null) break;
|
|
1050
|
+
pageParam = next;
|
|
1051
|
+
}
|
|
1052
|
+
if (myId !== this.currentFetchId || this.disposed) throw new DOMException("Superseded", "AbortError");
|
|
1053
|
+
const finalPages = previousPages.length > 0 && this.structuralShareEnabled && newPages.length > 0 ? [structuralShare(previousPages[0], newPages[0]), ...newPages.slice(1)] : newPages;
|
|
720
1054
|
batch(() => {
|
|
721
|
-
this.pages.set(
|
|
722
|
-
this.pageParams.set(
|
|
1055
|
+
this.pages.set(finalPages);
|
|
1056
|
+
this.pageParams.set(newParams);
|
|
723
1057
|
this.error.set(void 0);
|
|
724
1058
|
this.status.set("success");
|
|
725
1059
|
this.isLoading.set(false);
|
|
@@ -727,13 +1061,18 @@ var InfiniteEntry = class {
|
|
|
727
1061
|
this.lastUpdatedAt.set(Date.now());
|
|
728
1062
|
this.isStale.set(this.staleTime === 0);
|
|
729
1063
|
});
|
|
1064
|
+
this.forcedStale = false;
|
|
730
1065
|
if (this.staleTime > 0) this.scheduleStaleness();
|
|
731
1066
|
this.onSuccessData?.(this.pages.peek());
|
|
732
|
-
|
|
1067
|
+
return finalPages[0];
|
|
1068
|
+
} finally {
|
|
1069
|
+
if (!this.disposed && this.status.peek() === "pending" && !this.isFetching.peek() && this.pages.peek().length > 0) this.status.set("success");
|
|
1070
|
+
}
|
|
733
1071
|
}
|
|
734
1072
|
fetchNextPage() {
|
|
735
1073
|
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Entry disposed"));
|
|
736
1074
|
if (this.isFetchingNextPage.peek()) return Promise.resolve();
|
|
1075
|
+
if (this.networkMode === "online" && this.isOffline()) return this.scheduleDeferredFetch("next");
|
|
737
1076
|
const ps = this.pages.peek();
|
|
738
1077
|
if (ps.length === 0) return this.startFetch().then(() => {});
|
|
739
1078
|
const nextParam = this.getNextPageParam(ps[ps.length - 1], ps);
|
|
@@ -751,6 +1090,8 @@ var InfiniteEntry = class {
|
|
|
751
1090
|
batch(() => {
|
|
752
1091
|
this.pages.set([...this.pages.peek(), page]);
|
|
753
1092
|
this.pageParams.set([...this.pageParams.peek(), param]);
|
|
1093
|
+
this.error.set(void 0);
|
|
1094
|
+
this.status.set("success");
|
|
754
1095
|
this.isFetchingNextPage.set(false);
|
|
755
1096
|
this.isFetching.set(false);
|
|
756
1097
|
this.lastUpdatedAt.set(Date.now());
|
|
@@ -762,6 +1103,7 @@ var InfiniteEntry = class {
|
|
|
762
1103
|
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Entry disposed"));
|
|
763
1104
|
if (this.isFetchingPreviousPage.peek()) return Promise.resolve();
|
|
764
1105
|
if (!this.getPreviousPageParam) return Promise.resolve();
|
|
1106
|
+
if (this.networkMode === "online" && this.isOffline()) return this.scheduleDeferredFetch("prev");
|
|
765
1107
|
const ps = this.pages.peek();
|
|
766
1108
|
if (ps.length === 0) return this.startFetch().then(() => {});
|
|
767
1109
|
const prevParam = this.getPreviousPageParam(ps[0], ps);
|
|
@@ -779,6 +1121,8 @@ var InfiniteEntry = class {
|
|
|
779
1121
|
batch(() => {
|
|
780
1122
|
this.pages.set([page, ...this.pages.peek()]);
|
|
781
1123
|
this.pageParams.set([param, ...this.pageParams.peek()]);
|
|
1124
|
+
this.error.set(void 0);
|
|
1125
|
+
this.status.set("success");
|
|
782
1126
|
this.isFetchingPreviousPage.set(false);
|
|
783
1127
|
this.isFetching.set(false);
|
|
784
1128
|
this.lastUpdatedAt.set(Date.now());
|
|
@@ -814,7 +1158,7 @@ var InfiniteEntry = class {
|
|
|
814
1158
|
});
|
|
815
1159
|
throw err;
|
|
816
1160
|
}
|
|
817
|
-
await abortableSleep(
|
|
1161
|
+
await abortableSleep(this.computeRetryDelay(attempt), signal);
|
|
818
1162
|
attempt += 1;
|
|
819
1163
|
}
|
|
820
1164
|
}
|
|
@@ -822,18 +1166,25 @@ var InfiniteEntry = class {
|
|
|
822
1166
|
if (!succeeded) batch(() => {
|
|
823
1167
|
if (direction === "next") this.isFetchingNextPage.set(false);
|
|
824
1168
|
if (direction === "prev") this.isFetchingPreviousPage.set(false);
|
|
1169
|
+
if (!this.disposed && this.status.peek() === "pending" && !this.isFetching.peek() && this.pages.peek().length > 0) this.status.set("success");
|
|
825
1170
|
});
|
|
826
1171
|
}
|
|
827
1172
|
}
|
|
828
1173
|
refetch() {
|
|
829
1174
|
return this.startFetch();
|
|
830
1175
|
}
|
|
831
|
-
|
|
1176
|
+
/** Force stale without fetching — see `Entry.markStale` (spec §5.7, T3.9). */
|
|
1177
|
+
markStale() {
|
|
1178
|
+
if (this.disposed) return;
|
|
832
1179
|
if (this.staleTimer != null) {
|
|
833
1180
|
clearTimeout(this.staleTimer);
|
|
834
1181
|
this.staleTimer = null;
|
|
835
1182
|
}
|
|
1183
|
+
this.forcedStale = true;
|
|
836
1184
|
this.isStale.set(true);
|
|
1185
|
+
}
|
|
1186
|
+
invalidate() {
|
|
1187
|
+
this.markStale();
|
|
837
1188
|
return this.startFetch();
|
|
838
1189
|
}
|
|
839
1190
|
reset() {
|
|
@@ -843,33 +1194,73 @@ var InfiniteEntry = class {
|
|
|
843
1194
|
this.status.set(this.pages.peek().length > 0 ? "success" : "idle");
|
|
844
1195
|
});
|
|
845
1196
|
}
|
|
846
|
-
|
|
1197
|
+
/** Cancel an in-flight fetch (initial/refetch or paging) without touching
|
|
1198
|
+
* pages. Supersedes + aborts the current request, then restores a settled
|
|
1199
|
+
* status. No-op when idle. Mirrors `Entry.cancel` (spec §5, §6.4, T3.4). */
|
|
1200
|
+
cancel() {
|
|
1201
|
+
if (this.disposed || !this.isFetching.peek()) return;
|
|
1202
|
+
this.currentFetchId += 1;
|
|
1203
|
+
this.currentAbort?.abort();
|
|
1204
|
+
this.currentAbort = null;
|
|
1205
|
+
batch(() => {
|
|
1206
|
+
this.isFetching.set(false);
|
|
1207
|
+
this.isLoading.set(false);
|
|
1208
|
+
this.isFetchingNextPage.set(false);
|
|
1209
|
+
this.isFetchingPreviousPage.set(false);
|
|
1210
|
+
this.status.set(this.pages.peek().length > 0 ? "success" : "idle");
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
setData(updater, opts) {
|
|
847
1214
|
if (this.disposed) return {
|
|
848
1215
|
rollback: () => {},
|
|
849
1216
|
finalize: () => {}
|
|
850
1217
|
};
|
|
851
1218
|
const prev = this.pages.peek();
|
|
1219
|
+
const prevParams = this.pageParams.peek();
|
|
852
1220
|
const next = updater(prev.length === 0 ? void 0 : prev);
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
id,
|
|
1221
|
+
const record = opts?.track ?? true ? {
|
|
1222
|
+
id: this.nextSnapshotId++,
|
|
856
1223
|
prev,
|
|
1224
|
+
prevParams,
|
|
857
1225
|
live: true
|
|
858
|
-
};
|
|
859
|
-
this.snapshots.push(record);
|
|
1226
|
+
} : null;
|
|
1227
|
+
if (record) this.snapshots.push(record);
|
|
1228
|
+
let nextParams = prevParams;
|
|
1229
|
+
if (next.length !== prevParams.length) if (next.length < prevParams.length) nextParams = prevParams.slice(0, next.length);
|
|
1230
|
+
else {
|
|
1231
|
+
const pad = prevParams[prevParams.length - 1];
|
|
1232
|
+
nextParams = prevParams.slice();
|
|
1233
|
+
for (let i = prevParams.length; i < next.length; i++) nextParams.push(pad);
|
|
1234
|
+
}
|
|
860
1235
|
batch(() => {
|
|
861
1236
|
this.pages.set(next);
|
|
1237
|
+
if (nextParams !== prevParams) this.pageParams.set(nextParams);
|
|
862
1238
|
if (this.status.peek() === "idle" || this.status.peek() === "pending") this.status.set("success");
|
|
863
1239
|
this.lastUpdatedAt.set(Date.now());
|
|
864
|
-
this.hasPendingMutations.set(true);
|
|
1240
|
+
if (record) this.hasPendingMutations.set(true);
|
|
865
1241
|
});
|
|
1242
|
+
if (!record) return {
|
|
1243
|
+
rollback: () => {},
|
|
1244
|
+
finalize: () => {}
|
|
1245
|
+
};
|
|
1246
|
+
const id = record.id;
|
|
866
1247
|
return {
|
|
867
1248
|
rollback: () => {
|
|
868
1249
|
if (!record.live || this.disposed) return;
|
|
869
1250
|
record.live = false;
|
|
870
1251
|
batch(() => {
|
|
871
|
-
this.
|
|
872
|
-
|
|
1252
|
+
const i = this.snapshots.indexOf(record);
|
|
1253
|
+
if (i !== -1) {
|
|
1254
|
+
if (i === this.snapshots.length - 1) {
|
|
1255
|
+
this.pages.set(record.prev);
|
|
1256
|
+
this.pageParams.set(record.prevParams);
|
|
1257
|
+
} else {
|
|
1258
|
+
const below = this.snapshots[i + 1];
|
|
1259
|
+
below.prev = record.prev;
|
|
1260
|
+
below.prevParams = record.prevParams;
|
|
1261
|
+
}
|
|
1262
|
+
this.snapshots.splice(i, 1);
|
|
1263
|
+
}
|
|
873
1264
|
this.hasPendingMutations.set(this.snapshots.some((s) => s.live));
|
|
874
1265
|
});
|
|
875
1266
|
},
|
|
@@ -904,10 +1295,61 @@ var InfiniteEntry = class {
|
|
|
904
1295
|
});
|
|
905
1296
|
}
|
|
906
1297
|
isStaleNow() {
|
|
1298
|
+
if (this.forcedStale) return true;
|
|
907
1299
|
const last = this.lastUpdatedAt.peek();
|
|
908
1300
|
if (last === void 0) return true;
|
|
909
1301
|
return Date.now() - last >= this.staleTime;
|
|
910
1302
|
}
|
|
1303
|
+
computeRetryDelay(attempt) {
|
|
1304
|
+
const d = this.retryDelay;
|
|
1305
|
+
if (d === void 0) return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
1306
|
+
return typeof d === "function" ? d(attempt) : d;
|
|
1307
|
+
}
|
|
1308
|
+
isOffline() {
|
|
1309
|
+
return typeof navigator !== "undefined" && navigator.onLine === false;
|
|
1310
|
+
}
|
|
1311
|
+
scheduleDeferredFetch(direction) {
|
|
1312
|
+
this.isPaused.set(true);
|
|
1313
|
+
if (this.reconnectUnsub === null) this.reconnectUnsub = subscribeReconnect(() => this.drainDeferred());
|
|
1314
|
+
return new Promise((resolve, reject) => {
|
|
1315
|
+
this.deferredResolvers.push({
|
|
1316
|
+
direction,
|
|
1317
|
+
resolve,
|
|
1318
|
+
reject
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
drainDeferred() {
|
|
1323
|
+
if (this.deferredResolvers.length === 0) return;
|
|
1324
|
+
if (this.disposed) return;
|
|
1325
|
+
this.isPaused.set(false);
|
|
1326
|
+
const pending = this.deferredResolvers;
|
|
1327
|
+
this.deferredResolvers = [];
|
|
1328
|
+
if (this.reconnectUnsub !== null) {
|
|
1329
|
+
this.reconnectUnsub();
|
|
1330
|
+
this.reconnectUnsub = null;
|
|
1331
|
+
}
|
|
1332
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1333
|
+
const order = [
|
|
1334
|
+
"initial",
|
|
1335
|
+
"prev",
|
|
1336
|
+
"next"
|
|
1337
|
+
];
|
|
1338
|
+
for (const d of pending) seen.add(d.direction);
|
|
1339
|
+
const run = async () => {
|
|
1340
|
+
for (const dir of order) {
|
|
1341
|
+
if (!seen.has(dir)) continue;
|
|
1342
|
+
if (dir === "initial") await this.startFetch();
|
|
1343
|
+
else if (dir === "next") await this.fetchNextPage();
|
|
1344
|
+
else await this.fetchPreviousPage();
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
run().then(() => {
|
|
1348
|
+
for (const p of pending) p.resolve();
|
|
1349
|
+
}, (err) => {
|
|
1350
|
+
for (const p of pending) p.reject(err);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
911
1353
|
scheduleStaleness() {
|
|
912
1354
|
if (this.staleTimer != null) clearTimeout(this.staleTimer);
|
|
913
1355
|
if (this.staleTime > 0) this.staleTimer = setTimeout(() => {
|
|
@@ -924,6 +1366,22 @@ var InfiniteEntry = class {
|
|
|
924
1366
|
}
|
|
925
1367
|
this.currentAbort?.abort();
|
|
926
1368
|
this.currentAbort = null;
|
|
1369
|
+
batch(() => {
|
|
1370
|
+
this.isFetching.set(false);
|
|
1371
|
+
this.isLoading.set(false);
|
|
1372
|
+
this.isFetchingNextPage.set(false);
|
|
1373
|
+
this.isFetchingPreviousPage.set(false);
|
|
1374
|
+
});
|
|
1375
|
+
if (this.reconnectUnsub !== null) {
|
|
1376
|
+
this.reconnectUnsub();
|
|
1377
|
+
this.reconnectUnsub = null;
|
|
1378
|
+
}
|
|
1379
|
+
if (this.deferredResolvers.length > 0) {
|
|
1380
|
+
const disposed = new DOMException("Entry disposed", "AbortError");
|
|
1381
|
+
const pending = this.deferredResolvers;
|
|
1382
|
+
this.deferredResolvers = [];
|
|
1383
|
+
for (const p of pending) p.reject(disposed);
|
|
1384
|
+
}
|
|
927
1385
|
if (this.pendingFirstValueRejects.length > 0) {
|
|
928
1386
|
const disposed = new DOMException("Entry disposed", "AbortError");
|
|
929
1387
|
const rejects = this.pendingFirstValueRejects;
|
|
@@ -937,7 +1395,7 @@ var InfiniteEntry = class {
|
|
|
937
1395
|
/**
|
|
938
1396
|
* Stable string hash of a key tuple. Two equal-by-content args produce the
|
|
939
1397
|
* same string regardless of property iteration order. Handles primitives,
|
|
940
|
-
* arrays, plain objects, Date.
|
|
1398
|
+
* arrays, plain objects, Date, BigInt, NaN, ±Infinity.
|
|
941
1399
|
*
|
|
942
1400
|
* Functions and symbols throw — keys must be serializable so distinct
|
|
943
1401
|
* subscribers can share entries.
|
|
@@ -945,13 +1403,23 @@ var InfiniteEntry = class {
|
|
|
945
1403
|
function stableHash(args) {
|
|
946
1404
|
return JSON.stringify(args, replacer);
|
|
947
1405
|
}
|
|
948
|
-
const replacer = (
|
|
1406
|
+
const replacer = function(key, _value) {
|
|
1407
|
+
const value = this[key];
|
|
949
1408
|
if (typeof value === "function") throw new Error("[olas] query keys cannot contain functions");
|
|
950
1409
|
if (typeof value === "symbol") throw new Error("[olas] query keys cannot contain symbols");
|
|
1410
|
+
if (typeof value === "bigint") return { __bigint: value.toString() };
|
|
1411
|
+
if (typeof value === "number") {
|
|
1412
|
+
if (Number.isNaN(value)) return "__nan__";
|
|
1413
|
+
if (value === Number.POSITIVE_INFINITY) return "__+inf__";
|
|
1414
|
+
if (value === Number.NEGATIVE_INFINITY) return "__-inf__";
|
|
1415
|
+
return value;
|
|
1416
|
+
}
|
|
951
1417
|
if (value === void 0) return "__undefined__";
|
|
952
1418
|
if (value instanceof Date) return { __date: value.toISOString() };
|
|
953
1419
|
if (value instanceof Map || value instanceof Set) throw new Error("[olas] query keys cannot contain Map/Set — use arrays/objects");
|
|
954
1420
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1421
|
+
const proto = Object.getPrototypeOf(value);
|
|
1422
|
+
if (proto !== null && proto !== Object.prototype) throw new Error(`[olas] query keys cannot contain class instances (got ${proto.constructor?.name ?? "unknown"}) — pass plain object/array data`);
|
|
955
1423
|
const sorted = {};
|
|
956
1424
|
for (const k of Object.keys(value).sort()) sorted[k] = value[k];
|
|
957
1425
|
return sorted;
|
|
@@ -960,12 +1428,31 @@ const replacer = (_key, value) => {
|
|
|
960
1428
|
};
|
|
961
1429
|
//#endregion
|
|
962
1430
|
//#region src/query/plugin.ts
|
|
963
|
-
|
|
1431
|
+
/**
|
|
1432
|
+
* Back a module-level registry on `globalThis` under a shared `Symbol.for`
|
|
1433
|
+
* key. Guards the dual-package hazard: if a consumer ends up with both the ESM
|
|
1434
|
+
* and CJS build of `@kontsedal/olas-core` (common with mixed bundler configs),
|
|
1435
|
+
* each copy would otherwise own a SEPARATE registry `Map` — a mutation
|
|
1436
|
+
* registered via the ESM copy would be invisible to the mutation-queue plugin
|
|
1437
|
+
* loaded against the CJS copy, and cross-tab query lookups would miss. Sharing
|
|
1438
|
+
* one Map on `globalThis` makes both copies agree. T3.9.
|
|
1439
|
+
*/
|
|
1440
|
+
function globalRegistry(key) {
|
|
1441
|
+
const store = globalThis;
|
|
1442
|
+
const existing = store[key];
|
|
1443
|
+
if (existing) return existing;
|
|
1444
|
+
const created = /* @__PURE__ */ new Map();
|
|
1445
|
+
store[key] = created;
|
|
1446
|
+
return created;
|
|
1447
|
+
}
|
|
1448
|
+
const queryRegistry = globalRegistry(Symbol.for("olas.queryRegistry"));
|
|
964
1449
|
/**
|
|
965
1450
|
* Register a query by its `queryId`. Internal — called from `defineQuery` /
|
|
966
1451
|
* `defineInfiniteQuery`. Replaces any previous registration with the same
|
|
967
1452
|
* id (matches Olas's "full root rebuild" HMR story; a mid-flight remote
|
|
968
|
-
* message routed against the old `Query` simply misses).
|
|
1453
|
+
* message routed against the old `Query` simply misses). Dev-warns when a
|
|
1454
|
+
* *different* query overwrites an existing id — a genuine collision, since
|
|
1455
|
+
* cross-tab / persistence route by `queryId` (expected during HMR).
|
|
969
1456
|
*/
|
|
970
1457
|
function registerQueryById(queryId, query) {
|
|
971
1458
|
queryRegistry.set(queryId, query);
|
|
@@ -978,7 +1465,7 @@ function registerQueryById(queryId, query) {
|
|
|
978
1465
|
function lookupRegisteredQuery(queryId) {
|
|
979
1466
|
return queryRegistry.get(queryId);
|
|
980
1467
|
}
|
|
981
|
-
const mutationRegistry =
|
|
1468
|
+
const mutationRegistry = globalRegistry(Symbol.for("olas.mutationRegistry"));
|
|
982
1469
|
/** Register a mutation by its `mutationId`. Internal — called from `defineMutation`. */
|
|
983
1470
|
function registerMutationById(mutationId, entry) {
|
|
984
1471
|
mutationRegistry.set(mutationId, entry);
|
|
@@ -1000,6 +1487,14 @@ function _unregisterMutationById(mutationId) {
|
|
|
1000
1487
|
//#endregion
|
|
1001
1488
|
//#region src/query/client.ts
|
|
1002
1489
|
const DEFAULT_GC_TIME = 5 * 6e4;
|
|
1490
|
+
/**
|
|
1491
|
+
* Composite hydration-buffer key: query identity + key hash. Namespacing by
|
|
1492
|
+
* identity stops a dehydrated entry for query A being adopted by query B that
|
|
1493
|
+
* merely hashes to the same key (spec §15, T1.2). `JSON.stringify` of the pair
|
|
1494
|
+
* is used rather than string concatenation so no separator char is ambiguous —
|
|
1495
|
+
* both `id` (an arbitrary user `queryId`) and `hash` are unbounded strings.
|
|
1496
|
+
*/
|
|
1497
|
+
const hydrationKey = (id, hash) => JSON.stringify([id, hash]);
|
|
1003
1498
|
var ClientEntry = class {
|
|
1004
1499
|
entry;
|
|
1005
1500
|
/** The result of `spec.key(...args)` — used for hashing/identity. */
|
|
@@ -1038,6 +1533,8 @@ var ClientEntry = class {
|
|
|
1038
1533
|
staleTime: spec.staleTime,
|
|
1039
1534
|
retry: spec.retry,
|
|
1040
1535
|
retryDelay: spec.retryDelay,
|
|
1536
|
+
networkMode: spec.networkMode,
|
|
1537
|
+
structuralShare: spec.structuralShare,
|
|
1041
1538
|
initialData: hydrated?.data,
|
|
1042
1539
|
initialUpdatedAt: hydrated?.lastUpdatedAt,
|
|
1043
1540
|
events: void 0,
|
|
@@ -1075,6 +1572,8 @@ var ClientEntry = class {
|
|
|
1075
1572
|
if (this.refetchInterval == null) return;
|
|
1076
1573
|
if (this.intervalTimer != null) return;
|
|
1077
1574
|
this.intervalTimer = setInterval(() => {
|
|
1575
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1576
|
+
if (this.entry.isFetching.peek()) return;
|
|
1078
1577
|
this.entry.startFetch().catch(() => {});
|
|
1079
1578
|
}, this.refetchInterval);
|
|
1080
1579
|
}
|
|
@@ -1118,6 +1617,7 @@ var ClientEntry = class {
|
|
|
1118
1617
|
/** Refetch on focus / reconnect, but only if the data is actually stale. */
|
|
1119
1618
|
triggerEventRefetch() {
|
|
1120
1619
|
if (!this.entry.isStaleNow()) return;
|
|
1620
|
+
if (this.entry.isFetching.peek()) return;
|
|
1121
1621
|
this.entry.startFetch().catch(() => {});
|
|
1122
1622
|
}
|
|
1123
1623
|
dispose() {
|
|
@@ -1163,6 +1663,8 @@ var InfiniteClientEntry = class {
|
|
|
1163
1663
|
staleTime: spec.staleTime,
|
|
1164
1664
|
retry: spec.retry,
|
|
1165
1665
|
retryDelay: spec.retryDelay,
|
|
1666
|
+
networkMode: spec.networkMode,
|
|
1667
|
+
structuralShare: spec.structuralShare,
|
|
1166
1668
|
onSuccessData: onFetchSuccess
|
|
1167
1669
|
});
|
|
1168
1670
|
}
|
|
@@ -1174,6 +1676,9 @@ var InfiniteClientEntry = class {
|
|
|
1174
1676
|
}
|
|
1175
1677
|
if (this.subscriberCount === 1 && this.refetchInterval != null) this.startIntervalTimer();
|
|
1176
1678
|
}
|
|
1679
|
+
hasSubscribers() {
|
|
1680
|
+
return this.subscriberCount > 0;
|
|
1681
|
+
}
|
|
1177
1682
|
release() {
|
|
1178
1683
|
this.subscriberCount -= 1;
|
|
1179
1684
|
if (this.subscriberCount <= 0) {
|
|
@@ -1188,6 +1693,8 @@ var InfiniteClientEntry = class {
|
|
|
1188
1693
|
startIntervalTimer() {
|
|
1189
1694
|
if (this.refetchInterval == null || this.intervalTimer != null) return;
|
|
1190
1695
|
this.intervalTimer = setInterval(() => {
|
|
1696
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1697
|
+
if (this.entry.isFetching.peek()) return;
|
|
1191
1698
|
this.entry.startFetch().catch(() => {});
|
|
1192
1699
|
}, this.refetchInterval);
|
|
1193
1700
|
}
|
|
@@ -1263,7 +1770,7 @@ var QueryClient = class {
|
|
|
1263
1770
|
this.plugins = opts?.plugins ?? [];
|
|
1264
1771
|
if (opts?.hydrate) this.hydrate(opts.hydrate);
|
|
1265
1772
|
const api = this.makePluginApi();
|
|
1266
|
-
for (const plugin of this.plugins) this.callPlugin(() => plugin.init?.(api));
|
|
1773
|
+
for (const plugin of this.plugins) this.callPlugin(plugin, () => plugin.init?.(api));
|
|
1267
1774
|
}
|
|
1268
1775
|
/**
|
|
1269
1776
|
* Build the `QueryClientPluginApi` view that plugins receive at `init`
|
|
@@ -1288,13 +1795,14 @@ var QueryClient = class {
|
|
|
1288
1795
|
};
|
|
1289
1796
|
}
|
|
1290
1797
|
/** Invoke a plugin callback; route exceptions through `onError`. */
|
|
1291
|
-
callPlugin(fn) {
|
|
1798
|
+
callPlugin(plugin, fn) {
|
|
1292
1799
|
try {
|
|
1293
1800
|
fn();
|
|
1294
1801
|
} catch (err) {
|
|
1295
1802
|
dispatchError(this.onError, err, {
|
|
1296
1803
|
kind: "plugin",
|
|
1297
|
-
controllerPath: []
|
|
1804
|
+
controllerPath: [],
|
|
1805
|
+
pluginName: plugin.name
|
|
1298
1806
|
});
|
|
1299
1807
|
}
|
|
1300
1808
|
}
|
|
@@ -1329,7 +1837,7 @@ var QueryClient = class {
|
|
|
1329
1837
|
};
|
|
1330
1838
|
for (const plugin of this.plugins) if (plugin.onSetData) {
|
|
1331
1839
|
const cb = plugin.onSetData;
|
|
1332
|
-
this.callPlugin(() => cb.call(plugin, event));
|
|
1840
|
+
this.callPlugin(plugin, () => cb.call(plugin, event));
|
|
1333
1841
|
}
|
|
1334
1842
|
}
|
|
1335
1843
|
emitInvalidate(query, keyArgs, kind) {
|
|
@@ -1344,7 +1852,7 @@ var QueryClient = class {
|
|
|
1344
1852
|
};
|
|
1345
1853
|
for (const plugin of this.plugins) if (plugin.onInvalidate) {
|
|
1346
1854
|
const cb = plugin.onInvalidate;
|
|
1347
|
-
this.callPlugin(() => cb.call(plugin, event));
|
|
1855
|
+
this.callPlugin(plugin, () => cb.call(plugin, event));
|
|
1348
1856
|
}
|
|
1349
1857
|
}
|
|
1350
1858
|
emitGc(query, keyArgs, kind) {
|
|
@@ -1358,7 +1866,7 @@ var QueryClient = class {
|
|
|
1358
1866
|
};
|
|
1359
1867
|
for (const plugin of this.plugins) if (plugin.onGc) {
|
|
1360
1868
|
const cb = plugin.onGc;
|
|
1361
|
-
this.callPlugin(() => cb.call(plugin, event));
|
|
1869
|
+
this.callPlugin(plugin, () => cb.call(plugin, event));
|
|
1362
1870
|
}
|
|
1363
1871
|
}
|
|
1364
1872
|
/**
|
|
@@ -1370,7 +1878,7 @@ var QueryClient = class {
|
|
|
1370
1878
|
if (this.plugins.length === 0) return;
|
|
1371
1879
|
for (const plugin of this.plugins) if (plugin.onMutationEnqueue) {
|
|
1372
1880
|
const cb = plugin.onMutationEnqueue;
|
|
1373
|
-
this.callPlugin(() => cb.call(plugin, event));
|
|
1881
|
+
this.callPlugin(plugin, () => cb.call(plugin, event));
|
|
1374
1882
|
}
|
|
1375
1883
|
}
|
|
1376
1884
|
/** Fan out a `MutationSettleEvent` to every installed plugin. SPEC §13.3. */
|
|
@@ -1378,7 +1886,7 @@ var QueryClient = class {
|
|
|
1378
1886
|
if (this.plugins.length === 0) return;
|
|
1379
1887
|
for (const plugin of this.plugins) if (plugin.onMutationSettle) {
|
|
1380
1888
|
const cb = plugin.onMutationSettle;
|
|
1381
|
-
this.callPlugin(() => cb.call(plugin, event));
|
|
1889
|
+
this.callPlugin(plugin, () => cb.call(plugin, event));
|
|
1382
1890
|
}
|
|
1383
1891
|
}
|
|
1384
1892
|
/** Resolve `queryId → live entry-map keys`. Empty array when unknown. */
|
|
@@ -1422,13 +1930,52 @@ var QueryClient = class {
|
|
|
1422
1930
|
if (!entry) return;
|
|
1423
1931
|
this.applyingRemote = true;
|
|
1424
1932
|
try {
|
|
1425
|
-
entry.entry.setData(() => data);
|
|
1933
|
+
entry.entry.setData(() => data, { track: false });
|
|
1426
1934
|
this.emitSetData(internal, entry.keyArgs, data, "data", "remote");
|
|
1427
1935
|
} finally {
|
|
1428
1936
|
this.applyingRemote = false;
|
|
1429
1937
|
}
|
|
1430
1938
|
}
|
|
1431
1939
|
/**
|
|
1940
|
+
* Apply a single dehydrated entry to the cache. Idempotent across
|
|
1941
|
+
* pre-bind / post-bind:
|
|
1942
|
+
*
|
|
1943
|
+
* - If the matching `ClientEntry` already exists (a subscriber has
|
|
1944
|
+
* bound this key), `Entry.applyHydration(data, lastUpdatedAt)` writes
|
|
1945
|
+
* through with the server's timestamp + supersedes any inflight
|
|
1946
|
+
* fetch. Plugins see a `SetDataEvent` with `source: 'remote'`.
|
|
1947
|
+
* - If no `ClientEntry` exists yet (the subscribing component hasn't
|
|
1948
|
+
* mounted), the entry is buffered in `hydratedData` so the next
|
|
1949
|
+
* `bindEntry` for that hash picks it up — same path the constructor
|
|
1950
|
+
* `hydrate(state)` uses.
|
|
1951
|
+
*
|
|
1952
|
+
* Designed for streaming SSR: each `<Suspense>` boundary that resolves
|
|
1953
|
+
* on the server pushes its dehydrated entry through this method on the
|
|
1954
|
+
* client as the bootstrap script executes.
|
|
1955
|
+
*/
|
|
1956
|
+
applyDehydratedEntry(queryId, keyArgs, data, lastUpdatedAt) {
|
|
1957
|
+
const query = lookupRegisteredQuery(queryId);
|
|
1958
|
+
const hash = stableHash(keyArgs);
|
|
1959
|
+
if (query && query.__olas === "query") {
|
|
1960
|
+
const internal = query;
|
|
1961
|
+
const entry = this.maps.get(internal)?.get(hash);
|
|
1962
|
+
if (entry !== void 0) {
|
|
1963
|
+
this.applyingRemote = true;
|
|
1964
|
+
try {
|
|
1965
|
+
entry.entry.applyHydration(data, lastUpdatedAt);
|
|
1966
|
+
this.emitSetData(internal, entry.keyArgs, data, "data", "remote");
|
|
1967
|
+
} finally {
|
|
1968
|
+
this.applyingRemote = false;
|
|
1969
|
+
}
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
this.hydratedData.set(hydrationKey(queryId, hash), {
|
|
1974
|
+
data,
|
|
1975
|
+
lastUpdatedAt
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1432
1979
|
* Local-originated `setData` keyed by `queryId + keyArgs`. Plugin-facing
|
|
1433
1980
|
* (exposed via `QueryClientPluginApi.setEntryData`); used by the
|
|
1434
1981
|
* `@kontsedal/olas-entities` plugin to backpropagate entity patches into
|
|
@@ -1449,7 +1996,7 @@ var QueryClient = class {
|
|
|
1449
1996
|
if (!map) return;
|
|
1450
1997
|
const entry = map.get(hash);
|
|
1451
1998
|
if (!entry) return;
|
|
1452
|
-
entry.entry.setData(updater);
|
|
1999
|
+
entry.entry.setData(updater, { track: false });
|
|
1453
2000
|
this.emitSetData(internal, entry.keyArgs, entry.entry.data.peek(), "data", "set");
|
|
1454
2001
|
return;
|
|
1455
2002
|
}
|
|
@@ -1458,7 +2005,7 @@ var QueryClient = class {
|
|
|
1458
2005
|
if (!map) return;
|
|
1459
2006
|
const entry = map.get(hash);
|
|
1460
2007
|
if (!entry) return;
|
|
1461
|
-
entry.entry.setData(updater);
|
|
2008
|
+
entry.entry.setData(updater, { track: false });
|
|
1462
2009
|
this.emitSetData(internal, entry.keyArgs, entry.entry.pages.peek(), "infinite", "set");
|
|
1463
2010
|
}
|
|
1464
2011
|
applyRemoteInvalidate(queryId, keyArgs) {
|
|
@@ -1490,7 +2037,7 @@ var QueryClient = class {
|
|
|
1490
2037
|
if (state.version !== 1) return;
|
|
1491
2038
|
for (const entry of state.entries) {
|
|
1492
2039
|
const hash = stableHash(entry.key);
|
|
1493
|
-
this.hydratedData.set(hash, {
|
|
2040
|
+
this.hydratedData.set(hydrationKey(entry.id, hash), {
|
|
1494
2041
|
data: entry.data,
|
|
1495
2042
|
lastUpdatedAt: entry.lastUpdatedAt
|
|
1496
2043
|
});
|
|
@@ -1528,7 +2075,8 @@ var QueryClient = class {
|
|
|
1528
2075
|
}
|
|
1529
2076
|
dehydrate() {
|
|
1530
2077
|
const entries = [];
|
|
1531
|
-
for (const map of this.maps
|
|
2078
|
+
for (const [query, map] of this.maps) for (const ce of map.values()) if (ce.entry.status.peek() === "success") entries.push({
|
|
2079
|
+
id: query.__id,
|
|
1532
2080
|
key: ce.keyArgs,
|
|
1533
2081
|
data: ce.entry.data.peek(),
|
|
1534
2082
|
lastUpdatedAt: ce.entry.lastUpdatedAt.peek() ?? Date.now()
|
|
@@ -1554,6 +2102,19 @@ var QueryClient = class {
|
|
|
1554
2102
|
if (tasks.length === 0) return;
|
|
1555
2103
|
await Promise.all(tasks);
|
|
1556
2104
|
}
|
|
2105
|
+
const unsettled = [];
|
|
2106
|
+
for (const map of this.maps.values()) for (const ce of map.values()) if (ce.entry.isFetching.peek()) unsettled.push({
|
|
2107
|
+
key: ce.keyArgs,
|
|
2108
|
+
kind: "data"
|
|
2109
|
+
});
|
|
2110
|
+
for (const map of this.infiniteMaps.values()) for (const ce of map.values()) if (ce.entry.isFetching.peek()) unsettled.push({
|
|
2111
|
+
key: ce.keyArgs,
|
|
2112
|
+
kind: "infinite"
|
|
2113
|
+
});
|
|
2114
|
+
const err = /* @__PURE__ */ new Error("[olas] waitForIdle: exceeded 100-iteration safety bound — cache or mutations keep restarting");
|
|
2115
|
+
err.unsettled = unsettled;
|
|
2116
|
+
err.mutationsInflight = this.mutationsInflight$.peek();
|
|
2117
|
+
throw err;
|
|
1557
2118
|
}
|
|
1558
2119
|
bindEntry(query, args) {
|
|
1559
2120
|
const internal = query;
|
|
@@ -1568,8 +2129,9 @@ var QueryClient = class {
|
|
|
1568
2129
|
const hash = stableHash(keyArgs);
|
|
1569
2130
|
let entry = map.get(hash);
|
|
1570
2131
|
if (!entry) {
|
|
1571
|
-
const
|
|
1572
|
-
|
|
2132
|
+
const hkey = hydrationKey(internal.__id, hash);
|
|
2133
|
+
const hydrated = this.hydratedData.get(hkey);
|
|
2134
|
+
if (hydrated) this.hydratedData.delete(hkey);
|
|
1573
2135
|
const onFetchSuccess = internal.__spec.queryId != null ? (data) => this.emitSetData(internal, keyArgs, data, "data", "fetch") : void 0;
|
|
1574
2136
|
entry = new ClientEntry(this, internal, args, keyArgs, internal.__spec, hydrated, onFetchSuccess);
|
|
1575
2137
|
map.set(hash, entry);
|
|
@@ -1588,6 +2150,24 @@ var QueryClient = class {
|
|
|
1588
2150
|
if (map.size === 0) this.maps.delete(entry.query);
|
|
1589
2151
|
this.emitGc(entry.query, entry.keyArgs, "data");
|
|
1590
2152
|
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Invalidate one entry: if it has subscribers, mark stale AND refetch; if
|
|
2155
|
+
* not, mark stale only — the next subscriber refetches. Spec §5.7 says
|
|
2156
|
+
* invalidate refetches only IF subscribed; the old always-fetch behavior woke
|
|
2157
|
+
* orphaned entries that no one was watching (T3.9). Works for both
|
|
2158
|
+
* `ClientEntry` and `InfiniteClientEntry`.
|
|
2159
|
+
*/
|
|
2160
|
+
invalidateEntry(entry) {
|
|
2161
|
+
if (entry.hasSubscribers()) entry.entry.invalidate().catch((err) => {
|
|
2162
|
+
if (isAbortError(err)) return;
|
|
2163
|
+
dispatchError(this.onError, err, {
|
|
2164
|
+
kind: "cache",
|
|
2165
|
+
controllerPath: [],
|
|
2166
|
+
queryKey: entry.keyArgs
|
|
2167
|
+
});
|
|
2168
|
+
});
|
|
2169
|
+
else entry.entry.markStale();
|
|
2170
|
+
}
|
|
1591
2171
|
invalidate(query, args) {
|
|
1592
2172
|
const internal = query;
|
|
1593
2173
|
const map = this.maps.get(internal);
|
|
@@ -1596,14 +2176,7 @@ var QueryClient = class {
|
|
|
1596
2176
|
const hash = stableHash(keyArgs);
|
|
1597
2177
|
const entry = map.get(hash);
|
|
1598
2178
|
if (!entry) return;
|
|
1599
|
-
|
|
1600
|
-
if (isAbortError(err)) return;
|
|
1601
|
-
dispatchError(this.onError, err, {
|
|
1602
|
-
kind: "cache",
|
|
1603
|
-
controllerPath: [],
|
|
1604
|
-
queryKey: keyArgs
|
|
1605
|
-
});
|
|
1606
|
-
});
|
|
2179
|
+
this.invalidateEntry(entry);
|
|
1607
2180
|
this.emitInvalidate(internal, keyArgs, "data");
|
|
1608
2181
|
}
|
|
1609
2182
|
invalidateAll(query) {
|
|
@@ -1611,22 +2184,35 @@ var QueryClient = class {
|
|
|
1611
2184
|
const map = this.maps.get(internal);
|
|
1612
2185
|
if (!map) return;
|
|
1613
2186
|
for (const [hash, entry] of map) {
|
|
1614
|
-
|
|
1615
|
-
if (isAbortError(err)) return;
|
|
1616
|
-
dispatchError(this.onError, err, {
|
|
1617
|
-
kind: "cache",
|
|
1618
|
-
controllerPath: [],
|
|
1619
|
-
queryKey: entry.keyArgs
|
|
1620
|
-
});
|
|
1621
|
-
});
|
|
2187
|
+
this.invalidateEntry(entry);
|
|
1622
2188
|
this.emitInvalidate(internal, entry.keyArgs, "data");
|
|
1623
2189
|
}
|
|
1624
2190
|
}
|
|
2191
|
+
cancel(query, args) {
|
|
2192
|
+
const internal = query;
|
|
2193
|
+
const map = this.maps.get(internal);
|
|
2194
|
+
if (!map) return;
|
|
2195
|
+
const hash = stableHash(internal.__spec.key(...args));
|
|
2196
|
+
map.get(hash)?.entry.cancel();
|
|
2197
|
+
}
|
|
2198
|
+
cancelAll(query) {
|
|
2199
|
+
const map = this.maps.get(query);
|
|
2200
|
+
if (!map) return;
|
|
2201
|
+
for (const entry of map.values()) entry.entry.cancel();
|
|
2202
|
+
}
|
|
1625
2203
|
setData(query, args, updater) {
|
|
1626
2204
|
const entry = this.bindEntry(query, args);
|
|
1627
2205
|
const snapshot = entry.entry.setData(updater);
|
|
1628
2206
|
this.emitSetData(entry.query, entry.keyArgs, entry.entry.data.peek(), "data", "set");
|
|
1629
|
-
return
|
|
2207
|
+
return {
|
|
2208
|
+
rollback: () => {
|
|
2209
|
+
const before = entry.entry.data.peek();
|
|
2210
|
+
snapshot.rollback();
|
|
2211
|
+
const after = entry.entry.data.peek();
|
|
2212
|
+
if (!Object.is(before, after)) this.emitSetData(entry.query, entry.keyArgs, after, "data", "set");
|
|
2213
|
+
},
|
|
2214
|
+
finalize: () => snapshot.finalize()
|
|
2215
|
+
};
|
|
1630
2216
|
}
|
|
1631
2217
|
bindInfiniteEntry(query, args) {
|
|
1632
2218
|
const internal = query;
|
|
@@ -1666,14 +2252,7 @@ var QueryClient = class {
|
|
|
1666
2252
|
const hash = stableHash(keyArgs);
|
|
1667
2253
|
const entry = map.get(hash);
|
|
1668
2254
|
if (!entry) return;
|
|
1669
|
-
|
|
1670
|
-
if (isAbortError(err)) return;
|
|
1671
|
-
dispatchError(this.onError, err, {
|
|
1672
|
-
kind: "cache",
|
|
1673
|
-
controllerPath: [],
|
|
1674
|
-
queryKey: entry.keyArgs
|
|
1675
|
-
});
|
|
1676
|
-
});
|
|
2255
|
+
this.invalidateEntry(entry);
|
|
1677
2256
|
this.emitInvalidate(internal, keyArgs, "infinite");
|
|
1678
2257
|
}
|
|
1679
2258
|
invalidateAllInfinite(query) {
|
|
@@ -1681,22 +2260,35 @@ var QueryClient = class {
|
|
|
1681
2260
|
const map = this.infiniteMaps.get(internal);
|
|
1682
2261
|
if (!map) return;
|
|
1683
2262
|
for (const entry of map.values()) {
|
|
1684
|
-
|
|
1685
|
-
if (isAbortError(err)) return;
|
|
1686
|
-
dispatchError(this.onError, err, {
|
|
1687
|
-
kind: "cache",
|
|
1688
|
-
controllerPath: [],
|
|
1689
|
-
queryKey: entry.keyArgs
|
|
1690
|
-
});
|
|
1691
|
-
});
|
|
2263
|
+
this.invalidateEntry(entry);
|
|
1692
2264
|
this.emitInvalidate(internal, entry.keyArgs, "infinite");
|
|
1693
2265
|
}
|
|
1694
2266
|
}
|
|
2267
|
+
cancelInfinite(query, args) {
|
|
2268
|
+
const internal = query;
|
|
2269
|
+
const map = this.infiniteMaps.get(internal);
|
|
2270
|
+
if (!map) return;
|
|
2271
|
+
const hash = stableHash(internal.__spec.key(...args));
|
|
2272
|
+
map.get(hash)?.entry.cancel();
|
|
2273
|
+
}
|
|
2274
|
+
cancelAllInfinite(query) {
|
|
2275
|
+
const map = this.infiniteMaps.get(query);
|
|
2276
|
+
if (!map) return;
|
|
2277
|
+
for (const entry of map.values()) entry.entry.cancel();
|
|
2278
|
+
}
|
|
1695
2279
|
setInfiniteData(query, args, updater) {
|
|
1696
2280
|
const entry = this.bindInfiniteEntry(query, args);
|
|
1697
2281
|
const snapshot = entry.entry.setData(updater);
|
|
1698
2282
|
this.emitSetData(entry.query, entry.keyArgs, entry.entry.pages.peek(), "infinite", "set");
|
|
1699
|
-
return
|
|
2283
|
+
return {
|
|
2284
|
+
rollback: () => {
|
|
2285
|
+
const before = entry.entry.pages.peek();
|
|
2286
|
+
snapshot.rollback();
|
|
2287
|
+
const after = entry.entry.pages.peek();
|
|
2288
|
+
if (!Object.is(before, after)) this.emitSetData(entry.query, entry.keyArgs, after, "infinite", "set");
|
|
2289
|
+
},
|
|
2290
|
+
finalize: () => snapshot.finalize()
|
|
2291
|
+
};
|
|
1700
2292
|
}
|
|
1701
2293
|
prefetchInfinite(query, args) {
|
|
1702
2294
|
const entry = this.bindInfiniteEntry(query, args);
|
|
@@ -1734,7 +2326,7 @@ var QueryClient = class {
|
|
|
1734
2326
|
this.hydratedData.clear();
|
|
1735
2327
|
for (const plugin of this.plugins) if (plugin.dispose) {
|
|
1736
2328
|
const cb = plugin.dispose;
|
|
1737
|
-
this.callPlugin(() => cb.call(plugin));
|
|
2329
|
+
this.callPlugin(plugin, () => cb.call(plugin));
|
|
1738
2330
|
}
|
|
1739
2331
|
}
|
|
1740
2332
|
};
|
|
@@ -1819,12 +2411,52 @@ function createEmitter(options) {
|
|
|
1819
2411
|
}
|
|
1820
2412
|
//#endregion
|
|
1821
2413
|
//#region src/forms/field.ts
|
|
2414
|
+
/**
|
|
2415
|
+
* Flatten a single validator result to plain message strings for a leaf field.
|
|
2416
|
+
* A leaf has no descendants, so a `FormIssue[]`'s paths don't route anywhere —
|
|
2417
|
+
* every issue's `message` applies to this field. `null` → no messages.
|
|
2418
|
+
*/
|
|
2419
|
+
function messagesFromResult(result) {
|
|
2420
|
+
if (result == null) return [];
|
|
2421
|
+
if (typeof result === "string") return [result];
|
|
2422
|
+
return result.map((issue) => issue.message);
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Structural equality used by `Field.set` to decide whether a write returns
|
|
2426
|
+
* the field to its initial value (clearing `isDirty`). Cheap path for
|
|
2427
|
+
* primitives + `Object.is`; deep walk for arrays and plain objects. Class
|
|
2428
|
+
* instances, Map, Set, Date fall back to reference identity — same trade-off
|
|
2429
|
+
* `structural-share.ts` makes for cache data.
|
|
2430
|
+
*/
|
|
2431
|
+
function isStructurallyEqual(a, b) {
|
|
2432
|
+
if (Object.is(a, b)) return true;
|
|
2433
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
2434
|
+
if (Array.isArray(a)) {
|
|
2435
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
2436
|
+
for (let i = 0; i < a.length; i++) if (!isStructurallyEqual(a[i], b[i])) return false;
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
if (Array.isArray(b)) return false;
|
|
2440
|
+
const protoA = Object.getPrototypeOf(a);
|
|
2441
|
+
const protoB = Object.getPrototypeOf(b);
|
|
2442
|
+
if (protoA !== Object.prototype && protoA !== null) return false;
|
|
2443
|
+
if (protoB !== Object.prototype && protoB !== null) return false;
|
|
2444
|
+
const keysA = Object.keys(a);
|
|
2445
|
+
const keysB = Object.keys(b);
|
|
2446
|
+
if (keysA.length !== keysB.length) return false;
|
|
2447
|
+
for (const k of keysA) {
|
|
2448
|
+
if (!Object.hasOwn(b, k)) return false;
|
|
2449
|
+
if (!isStructurallyEqual(a[k], b[k])) return false;
|
|
2450
|
+
}
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
1822
2453
|
var FieldImpl = class {
|
|
1823
2454
|
value$;
|
|
1824
2455
|
/**
|
|
1825
2456
|
* Validator-produced errors. The public `errors` getter merges this with
|
|
1826
|
-
* `serverErrors$` so consumers see a single flat array.
|
|
1827
|
-
* re-run of validators (after a new value) doesn't clobber
|
|
2457
|
+
* `serverErrors$` and `formErrors$` so consumers see a single flat array.
|
|
2458
|
+
* Kept separate so a re-run of validators (after a new value) doesn't clobber
|
|
2459
|
+
* the other two channels.
|
|
1828
2460
|
*/
|
|
1829
2461
|
validatorErrors$;
|
|
1830
2462
|
/**
|
|
@@ -1832,11 +2464,29 @@ var FieldImpl = class {
|
|
|
1832
2464
|
* `set()`, on `reset()`, or via an explicit `setErrors([])`.
|
|
1833
2465
|
*/
|
|
1834
2466
|
serverErrors$;
|
|
2467
|
+
/**
|
|
2468
|
+
* Errors routed here by a parent (or ancestor) form-level validator that
|
|
2469
|
+
* targeted this field via a `FormIssue` path — the third error channel
|
|
2470
|
+
* beside validator + server errors (T5.2). Owned by the routing form: cleared
|
|
2471
|
+
* and re-applied on every form-level validation run, so it does NOT clear on
|
|
2472
|
+
* the field's own `set()` (a stale cross-field error survives until the next
|
|
2473
|
+
* form-level run recomputes it). Cleared by `reset()` / `setAsInitial()`.
|
|
2474
|
+
*/
|
|
2475
|
+
formErrors$;
|
|
1835
2476
|
errors$;
|
|
1836
2477
|
touched$;
|
|
1837
2478
|
dirty$;
|
|
1838
2479
|
validating$;
|
|
1839
2480
|
isValid$;
|
|
2481
|
+
/**
|
|
2482
|
+
* Validity as of the last *settled* validation pass. While a pass is in
|
|
2483
|
+
* flight (`validating$`), `isValid` reads this instead of the live `errors`
|
|
2484
|
+
* — otherwise a `debouncedValidator` would flip `isValid` to `false` on every
|
|
2485
|
+
* keystroke (the async-start clears `validatorErrors$`), strobing a submit
|
|
2486
|
+
* button. Updated only when a pass completes (T5.3). Defaults `true` (an
|
|
2487
|
+
* untouched field with a pending first run reads valid, not invalid).
|
|
2488
|
+
*/
|
|
2489
|
+
lastValid$;
|
|
1840
2490
|
revalidateTrigger$;
|
|
1841
2491
|
validators;
|
|
1842
2492
|
/** The value `reset()` returns to. Mutated by `setAsInitial()` so a form
|
|
@@ -1848,25 +2498,42 @@ var FieldImpl = class {
|
|
|
1848
2498
|
disposed = false;
|
|
1849
2499
|
devtoolsOwner = null;
|
|
1850
2500
|
onValidatorError = null;
|
|
2501
|
+
validateOn;
|
|
2502
|
+
/** Reactive gate — when false, the validator effect skips its run. Flipped
|
|
2503
|
+
* on by the relevant trigger for the field's `validateOn` mode. Once on,
|
|
2504
|
+
* stays on for the lifetime of the field (matches RHF's `mode + reValidateMode`
|
|
2505
|
+
* default semantics: after the first activation, subsequent changes
|
|
2506
|
+
* re-validate). `reset()` flips it back to false. */
|
|
2507
|
+
validateUnlocked$;
|
|
1851
2508
|
constructor(initial, validators = [], options) {
|
|
1852
2509
|
this.initial = initial;
|
|
1853
2510
|
this.validators = validators;
|
|
1854
2511
|
this.onValidatorError = options?.onValidatorError ?? null;
|
|
2512
|
+
this.validateOn = options?.validateOn ?? "change";
|
|
1855
2513
|
this.value$ = signal(initial);
|
|
1856
2514
|
this.validatorErrors$ = signal([]);
|
|
1857
2515
|
this.serverErrors$ = signal([]);
|
|
2516
|
+
this.formErrors$ = signal([]);
|
|
1858
2517
|
this.touched$ = signal(false);
|
|
1859
2518
|
this.dirty$ = signal(false);
|
|
1860
2519
|
this.validating$ = signal(false);
|
|
2520
|
+
this.lastValid$ = signal(true);
|
|
1861
2521
|
this.revalidateTrigger$ = signal(0);
|
|
2522
|
+
this.validateUnlocked$ = signal(this.validateOn === "change");
|
|
1862
2523
|
this.errors$ = computed(() => {
|
|
1863
2524
|
const v = this.validatorErrors$.value;
|
|
1864
2525
|
const s = this.serverErrors$.value;
|
|
1865
|
-
|
|
1866
|
-
if (
|
|
1867
|
-
|
|
2526
|
+
const f = this.formErrors$.value;
|
|
2527
|
+
if (s.length === 0 && f.length === 0) return v;
|
|
2528
|
+
if (v.length === 0 && f.length === 0) return s;
|
|
2529
|
+
if (v.length === 0 && s.length === 0) return f;
|
|
2530
|
+
return [
|
|
2531
|
+
...v,
|
|
2532
|
+
...s,
|
|
2533
|
+
...f
|
|
2534
|
+
];
|
|
1868
2535
|
});
|
|
1869
|
-
this.isValid$ = computed(() => this.
|
|
2536
|
+
this.isValid$ = computed(() => this.validating$.value ? this.lastValid$.value : this.errors$.value.length === 0);
|
|
1870
2537
|
if (validators.length > 0) this.validatorDispose = effect(() => {
|
|
1871
2538
|
this.runValidators();
|
|
1872
2539
|
});
|
|
@@ -1887,6 +2554,9 @@ var FieldImpl = class {
|
|
|
1887
2554
|
subscribe(handler) {
|
|
1888
2555
|
return this.value$.subscribe(handler);
|
|
1889
2556
|
}
|
|
2557
|
+
subscribeChanges(handler) {
|
|
2558
|
+
return this.value$.subscribeChanges(handler);
|
|
2559
|
+
}
|
|
1890
2560
|
get errors() {
|
|
1891
2561
|
return this.errors$;
|
|
1892
2562
|
}
|
|
@@ -1906,7 +2576,7 @@ var FieldImpl = class {
|
|
|
1906
2576
|
if (this.disposed) return;
|
|
1907
2577
|
batch(() => {
|
|
1908
2578
|
this.value$.set(value);
|
|
1909
|
-
this.dirty$.set(
|
|
2579
|
+
this.dirty$.set(!isStructurallyEqual(value, this.initial));
|
|
1910
2580
|
if (this.serverErrors$.peek().length > 0) this.serverErrors$.set([]);
|
|
1911
2581
|
});
|
|
1912
2582
|
}
|
|
@@ -1916,6 +2586,18 @@ var FieldImpl = class {
|
|
|
1916
2586
|
this.serverErrors$.set(next);
|
|
1917
2587
|
}
|
|
1918
2588
|
/**
|
|
2589
|
+
* Internal — set the parent-form-validator error channel (`formErrors$`).
|
|
2590
|
+
* Called by the owning form's issue router when a `FormIssue` path resolves
|
|
2591
|
+
* to this field. Guards against churning the signal when nothing changes so
|
|
2592
|
+
* a whole-tree clear pass doesn't wake unrelated subscribers. See T5.2 /
|
|
2593
|
+
* `routeFormIssues` in `form.ts`.
|
|
2594
|
+
*/
|
|
2595
|
+
setFormErrors(errors) {
|
|
2596
|
+
if (this.disposed) return;
|
|
2597
|
+
if (this.formErrors$.peek().length === 0 && errors.length === 0) return;
|
|
2598
|
+
this.formErrors$.set(errors.length === 0 ? [] : [...errors]);
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
1919
2601
|
* Reseat the field as if this value had been its constructor `initial`.
|
|
1920
2602
|
* Sets the value, re-anchors `reset()`'s target, and does NOT mark dirty.
|
|
1921
2603
|
* Used by `Form` when applying its own `initial` (in the constructor and
|
|
@@ -1928,6 +2610,8 @@ var FieldImpl = class {
|
|
|
1928
2610
|
batch(() => {
|
|
1929
2611
|
this.value$.set(value);
|
|
1930
2612
|
this.dirty$.set(false);
|
|
2613
|
+
if (this.serverErrors$.peek().length > 0) this.serverErrors$.set([]);
|
|
2614
|
+
if (this.formErrors$.peek().length > 0) this.formErrors$.set([]);
|
|
1931
2615
|
});
|
|
1932
2616
|
}
|
|
1933
2617
|
reset() {
|
|
@@ -1940,15 +2624,19 @@ var FieldImpl = class {
|
|
|
1940
2624
|
this.touched$.set(false);
|
|
1941
2625
|
this.validatorErrors$.set([]);
|
|
1942
2626
|
this.serverErrors$.set([]);
|
|
2627
|
+
this.formErrors$.set([]);
|
|
1943
2628
|
this.validating$.set(false);
|
|
2629
|
+
if (this.validateOn !== "change") this.validateUnlocked$.set(false);
|
|
1944
2630
|
});
|
|
1945
2631
|
}
|
|
1946
2632
|
markTouched() {
|
|
1947
2633
|
if (this.disposed) return;
|
|
1948
2634
|
this.touched$.set(true);
|
|
2635
|
+
if (this.validateOn === "blur" && !this.validateUnlocked$.peek()) this.validateUnlocked$.set(true);
|
|
1949
2636
|
}
|
|
1950
2637
|
async revalidate() {
|
|
1951
2638
|
if (this.disposed) return this.isValid$.peek();
|
|
2639
|
+
if (!this.validateUnlocked$.peek()) this.validateUnlocked$.set(true);
|
|
1952
2640
|
this.revalidateTrigger$.update((n) => n + 1);
|
|
1953
2641
|
await this.waitUntilSettled();
|
|
1954
2642
|
return this.isValid$.peek();
|
|
@@ -1987,6 +2675,14 @@ var FieldImpl = class {
|
|
|
1987
2675
|
if (this.disposed) return;
|
|
1988
2676
|
const value = this.value$.value;
|
|
1989
2677
|
this.revalidateTrigger$.value;
|
|
2678
|
+
if (!this.validateUnlocked$.value) {
|
|
2679
|
+
batch(() => {
|
|
2680
|
+
if (this.validatorErrors$.peek().length > 0) this.validatorErrors$.set([]);
|
|
2681
|
+
if (this.validating$.peek()) this.validating$.set(false);
|
|
2682
|
+
this.lastValid$.set(this.errors$.peek().length === 0);
|
|
2683
|
+
});
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
1990
2686
|
this.currentAbort?.abort();
|
|
1991
2687
|
const abort = new AbortController();
|
|
1992
2688
|
this.currentAbort = abort;
|
|
@@ -1996,17 +2692,21 @@ var FieldImpl = class {
|
|
|
1996
2692
|
for (const validator of this.validators) try {
|
|
1997
2693
|
const result = validator(value, abort.signal);
|
|
1998
2694
|
if (result instanceof Promise) asyncPromises.push(result);
|
|
1999
|
-
else
|
|
2695
|
+
else {
|
|
2696
|
+
const msgs = messagesFromResult(result);
|
|
2697
|
+
if (msgs.length > 0) syncErrors.push(...msgs);
|
|
2698
|
+
}
|
|
2000
2699
|
} catch (err) {
|
|
2001
2700
|
try {
|
|
2002
2701
|
this.onValidatorError?.(err);
|
|
2003
2702
|
} catch {}
|
|
2004
|
-
syncErrors.push(
|
|
2703
|
+
syncErrors.push("Validation failed");
|
|
2005
2704
|
}
|
|
2006
2705
|
if (syncErrors.length > 0) {
|
|
2007
2706
|
batch(() => {
|
|
2008
2707
|
this.validatorErrors$.set(syncErrors);
|
|
2009
2708
|
this.validating$.set(false);
|
|
2709
|
+
this.lastValid$.set(this.errors$.peek().length === 0);
|
|
2010
2710
|
});
|
|
2011
2711
|
this.emitValidated(false, syncErrors);
|
|
2012
2712
|
return;
|
|
@@ -2015,6 +2715,7 @@ var FieldImpl = class {
|
|
|
2015
2715
|
batch(() => {
|
|
2016
2716
|
this.validatorErrors$.set([]);
|
|
2017
2717
|
this.validating$.set(false);
|
|
2718
|
+
this.lastValid$.set(this.errors$.peek().length === 0);
|
|
2018
2719
|
});
|
|
2019
2720
|
this.emitValidated(true, []);
|
|
2020
2721
|
return;
|
|
@@ -2026,15 +2727,15 @@ var FieldImpl = class {
|
|
|
2026
2727
|
Promise.allSettled(asyncPromises).then((results) => {
|
|
2027
2728
|
if (myId !== this.runId || this.disposed) return;
|
|
2028
2729
|
const asyncErrors = [];
|
|
2029
|
-
for (const r of results) if (r.status === "fulfilled")
|
|
2030
|
-
|
|
2031
|
-
} else if (!isAbortError(r.reason)) {
|
|
2730
|
+
for (const r of results) if (r.status === "fulfilled") asyncErrors.push(...messagesFromResult(r.value));
|
|
2731
|
+
else if (!isAbortError(r.reason)) {
|
|
2032
2732
|
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
2033
2733
|
asyncErrors.push(msg);
|
|
2034
2734
|
}
|
|
2035
2735
|
batch(() => {
|
|
2036
2736
|
this.validatorErrors$.set(asyncErrors);
|
|
2037
2737
|
this.validating$.set(false);
|
|
2738
|
+
this.lastValid$.set(this.errors$.peek().length === 0);
|
|
2038
2739
|
});
|
|
2039
2740
|
this.emitValidated(asyncErrors.length === 0, asyncErrors);
|
|
2040
2741
|
});
|
|
@@ -2065,7 +2766,9 @@ function createField(initial, validators, options) {
|
|
|
2065
2766
|
/**
|
|
2066
2767
|
* Wrap an async validator with a debounce. The debounce timer resets on every
|
|
2067
2768
|
* value change. While debouncing or the request is in flight, the field's
|
|
2068
|
-
* `isValidating` is true and `isValid`
|
|
2769
|
+
* `isValidating` is true and `isValid` HOLDS its last settled value (T5.3) — so
|
|
2770
|
+
* editing an already-valid field doesn't strobe a submit button to disabled on
|
|
2771
|
+
* every keystroke. A field with no prior settled validation defaults to valid.
|
|
2069
2772
|
*/
|
|
2070
2773
|
function debouncedValidator(fn, ms) {
|
|
2071
2774
|
return (value, signal) => new Promise((resolve, reject) => {
|
|
@@ -2091,6 +2794,73 @@ const FORM_BRAND = Symbol.for("olas.form");
|
|
|
2091
2794
|
const FIELD_ARRAY_BRAND = Symbol.for("olas.fieldArray");
|
|
2092
2795
|
const isForm = (x) => typeof x === "object" && x !== null && x[FORM_BRAND] === true;
|
|
2093
2796
|
const isFieldArray = (x) => typeof x === "object" && x !== null && x[FIELD_ARRAY_BRAND] === true;
|
|
2797
|
+
/**
|
|
2798
|
+
* Walk a form tree from `root` following `FormIssue.path` segments. Forms walk
|
|
2799
|
+
* keys; field-arrays walk numeric indices. Returns the resolved node, or
|
|
2800
|
+
* `undefined` if the path runs off the tree (bad index, or a leaf reached with
|
|
2801
|
+
* an unconsumed path segment). Reads are untracked (`fields` is a static object;
|
|
2802
|
+
* `at()` peeks) so calling this inside a validator effect adds no dependencies.
|
|
2803
|
+
*/
|
|
2804
|
+
function resolveNode(root, segments) {
|
|
2805
|
+
let cursor = root;
|
|
2806
|
+
for (const seg of segments) {
|
|
2807
|
+
if (cursor === void 0 || cursor === null) return void 0;
|
|
2808
|
+
if (isForm(cursor)) cursor = cursor.fields[String(seg)];
|
|
2809
|
+
else if (isFieldArray(cursor)) {
|
|
2810
|
+
const idx = Number(seg);
|
|
2811
|
+
if (!Number.isInteger(idx) || idx < 0) return void 0;
|
|
2812
|
+
cursor = cursor.at(idx);
|
|
2813
|
+
} else return;
|
|
2814
|
+
}
|
|
2815
|
+
return cursor;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Normalize one validator result into the shared `FormIssue[]` shape. A plain
|
|
2819
|
+
* string is a message on the node itself (empty path); `null` contributes
|
|
2820
|
+
* nothing; a `FormIssue[]` is appended verbatim.
|
|
2821
|
+
*/
|
|
2822
|
+
function appendIssues(out, result) {
|
|
2823
|
+
if (result == null) return;
|
|
2824
|
+
if (typeof result === "string") {
|
|
2825
|
+
out.push({
|
|
2826
|
+
path: [],
|
|
2827
|
+
message: result
|
|
2828
|
+
});
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
for (const issue of result) out.push(issue);
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Route a fully-collected issue set for one form-level validation run:
|
|
2835
|
+
* - empty-path (and unresolvable) issues → `topLevelErrors$` on the owning node
|
|
2836
|
+
* - path issues → the resolved descendant's `setFormErrors`
|
|
2837
|
+
*
|
|
2838
|
+
* Targets that received an error last run but not this one are cleared, so a
|
|
2839
|
+
* fixed cross-field rule removes its message from the field it landed on.
|
|
2840
|
+
* Returns the new target set for the caller to retain. MUST run inside a batch.
|
|
2841
|
+
*/
|
|
2842
|
+
function routeFormIssues(root, issues, topLevelErrors$, lastTargets) {
|
|
2843
|
+
const topLevel = [];
|
|
2844
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
2845
|
+
for (const issue of issues) {
|
|
2846
|
+
if (issue.path.length === 0) {
|
|
2847
|
+
topLevel.push(issue.message);
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
const target = resolveNode(root, issue.path);
|
|
2851
|
+
if (target === void 0 || typeof target.setFormErrors !== "function") {
|
|
2852
|
+
topLevel.push(issue.message);
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
const list = byTarget.get(target);
|
|
2856
|
+
if (list) list.push(issue.message);
|
|
2857
|
+
else byTarget.set(target, [issue.message]);
|
|
2858
|
+
}
|
|
2859
|
+
topLevelErrors$.set(topLevel);
|
|
2860
|
+
for (const t of lastTargets) if (!byTarget.has(t)) t.setFormErrors?.([]);
|
|
2861
|
+
for (const [t, msgs] of byTarget) t.setFormErrors?.(msgs);
|
|
2862
|
+
return new Set(byTarget.keys());
|
|
2863
|
+
}
|
|
2094
2864
|
var FormImpl = class {
|
|
2095
2865
|
[FORM_BRAND] = true;
|
|
2096
2866
|
fields;
|
|
@@ -2101,9 +2871,33 @@ var FormImpl = class {
|
|
|
2101
2871
|
touched;
|
|
2102
2872
|
isValidating;
|
|
2103
2873
|
flatErrors;
|
|
2874
|
+
/**
|
|
2875
|
+
* Dotted paths of every leaf whose `isDirty` is `true`. Recomputes when
|
|
2876
|
+
* any child's dirty state flips; ordered by depth-first traversal so two
|
|
2877
|
+
* snapshots of a stable tree compare with `===`-friendly references on
|
|
2878
|
+
* unchanged subsets. Useful for partial-update PATCH payloads and
|
|
2879
|
+
* "highlight the changed inputs" UIs.
|
|
2880
|
+
*/
|
|
2881
|
+
dirtyFields;
|
|
2104
2882
|
topLevelErrors$ = signal([]);
|
|
2105
|
-
|
|
2883
|
+
/**
|
|
2884
|
+
* Errors routed to THIS form by an ancestor form-level validator (a
|
|
2885
|
+
* `FormIssue` whose path resolves to this node). Merged into `topLevelErrors`
|
|
2886
|
+
* beside this form's own validator output, so `topLevelErrors` means "errors
|
|
2887
|
+
* attached to this node itself, whatever their source". Owned by the ancestor
|
|
2888
|
+
* router (T5.2) — see `setFormErrors`.
|
|
2889
|
+
*/
|
|
2890
|
+
parentFormErrors$ = signal([]);
|
|
2891
|
+
topLevelErrors = computed(() => {
|
|
2892
|
+
const own = this.topLevelErrors$.value;
|
|
2893
|
+
const parent = this.parentFormErrors$.value;
|
|
2894
|
+
if (parent.length === 0) return own;
|
|
2895
|
+
if (own.length === 0) return parent;
|
|
2896
|
+
return [...own, ...parent];
|
|
2897
|
+
});
|
|
2106
2898
|
topLevelValidating$ = signal(false);
|
|
2899
|
+
/** Targets written by the last form-level run — cleared next run if absent. */
|
|
2900
|
+
lastFormErrorTargets = /* @__PURE__ */ new Set();
|
|
2107
2901
|
isSubmitting$ = signal(false);
|
|
2108
2902
|
submitCount$ = signal(0);
|
|
2109
2903
|
submitError$ = signal(void 0);
|
|
@@ -2157,12 +2951,17 @@ var FormImpl = class {
|
|
|
2157
2951
|
return false;
|
|
2158
2952
|
});
|
|
2159
2953
|
this.isValid = computed(() => {
|
|
2160
|
-
if (this.topLevelErrors
|
|
2954
|
+
if (this.topLevelErrors.value.length > 0) return false;
|
|
2161
2955
|
if (this.isValidating.value) return false;
|
|
2162
2956
|
for (const child of Object.values(this.fields)) if (!child.isValid.value) return false;
|
|
2163
2957
|
return true;
|
|
2164
2958
|
});
|
|
2165
2959
|
this.flatErrors = computed(() => this.computeFlatErrors());
|
|
2960
|
+
this.dirtyFields = computed(() => {
|
|
2961
|
+
const out = [];
|
|
2962
|
+
collectDirtyFields(this.fields, "", out);
|
|
2963
|
+
return out;
|
|
2964
|
+
});
|
|
2166
2965
|
if (this.validators.length > 0) this.validatorDispose = effect(() => this.runTopLevelValidators());
|
|
2167
2966
|
}
|
|
2168
2967
|
computeValue() {
|
|
@@ -2187,7 +2986,7 @@ var FormImpl = class {
|
|
|
2187
2986
|
}
|
|
2188
2987
|
computeFlatErrors() {
|
|
2189
2988
|
const out = [];
|
|
2190
|
-
const tle = this.topLevelErrors
|
|
2989
|
+
const tle = this.topLevelErrors.value;
|
|
2191
2990
|
if (tle.length > 0) out.push({
|
|
2192
2991
|
path: "",
|
|
2193
2992
|
errors: tle
|
|
@@ -2243,11 +3042,14 @@ var FormImpl = class {
|
|
|
2243
3042
|
for (const child of Object.values(this.fields)) if (isForm(child) || isFieldArray(child)) child.reset();
|
|
2244
3043
|
else child.reset();
|
|
2245
3044
|
this.topLevelErrors$.set([]);
|
|
3045
|
+
this.parentFormErrors$.set([]);
|
|
3046
|
+
this.submitCount$.set(0);
|
|
3047
|
+
this.submitError$.set(void 0);
|
|
3048
|
+
if (this.options?.initial !== void 0) {
|
|
3049
|
+
const ini = typeof this.options.initial === "function" ? this.options.initial() : this.options.initial;
|
|
3050
|
+
if (ini !== void 0) this.applyPartial(ini, true);
|
|
3051
|
+
}
|
|
2246
3052
|
});
|
|
2247
|
-
if (this.options?.initial !== void 0) {
|
|
2248
|
-
const ini = typeof this.options.initial === "function" ? this.options.initial() : this.options.initial;
|
|
2249
|
-
if (ini !== void 0) this.applyPartial(ini, true);
|
|
2250
|
-
}
|
|
2251
3053
|
}
|
|
2252
3054
|
markAllTouched() {
|
|
2253
3055
|
if (this.disposed) return;
|
|
@@ -2349,9 +3151,43 @@ var FormImpl = class {
|
|
|
2349
3151
|
}
|
|
2350
3152
|
});
|
|
2351
3153
|
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Internal — receive errors routed here by an ancestor form-level validator
|
|
3156
|
+
* (a `FormIssue` whose path resolved to this nested form). Guards against
|
|
3157
|
+
* spurious writes so a whole-tree clear pass doesn't wake subscribers. See
|
|
3158
|
+
* `routeFormIssues`.
|
|
3159
|
+
*/
|
|
3160
|
+
setFormErrors(errors) {
|
|
3161
|
+
if (this.disposed) return;
|
|
3162
|
+
if (this.parentFormErrors$.peek().length === 0 && errors.length === 0) return;
|
|
3163
|
+
this.parentFormErrors$.set(errors.length === 0 ? [] : [...errors]);
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Reset a named subtree to its initial. `path` uses the same dotted /
|
|
3167
|
+
* bracket notation as `setErrors` / `flatErrors`. Useful when
|
|
3168
|
+
* `Form.set({foo: undefined})` would be ambiguous ("clear" vs "leave
|
|
3169
|
+
* alone" — the latter is what `set` does today).
|
|
3170
|
+
*
|
|
3171
|
+
* Walks via `resolvePath`; the resolved target must expose `reset()`
|
|
3172
|
+
* (Field, Form, or FieldArray all do). Unknown paths are silently
|
|
3173
|
+
* ignored — `flatErrors`-shaped paths from upstream code shouldn't
|
|
3174
|
+
* crash this. Pass an empty string to reset the whole form (same as
|
|
3175
|
+
* `form.reset()`).
|
|
3176
|
+
*/
|
|
3177
|
+
clearSubtree(path) {
|
|
3178
|
+
if (this.disposed) return;
|
|
3179
|
+
if (path === "") {
|
|
3180
|
+
this.reset();
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
const target = this.resolvePath(path);
|
|
3184
|
+
if (target === void 0) return;
|
|
3185
|
+
if (typeof target.reset === "function") target.reset();
|
|
3186
|
+
}
|
|
2352
3187
|
resolvePath(path) {
|
|
2353
3188
|
if (path === "") return void 0;
|
|
2354
|
-
const segments = path
|
|
3189
|
+
const segments = splitPath(path);
|
|
3190
|
+
if (segments === null) return void 0;
|
|
2355
3191
|
let cursor = this;
|
|
2356
3192
|
for (const seg of segments) {
|
|
2357
3193
|
if (cursor === void 0 || cursor === null) return void 0;
|
|
@@ -2388,47 +3224,99 @@ var FormImpl = class {
|
|
|
2388
3224
|
const abort = new AbortController();
|
|
2389
3225
|
this.currentValidatorAbort = abort;
|
|
2390
3226
|
const myId = ++this.currentValidatorRun;
|
|
2391
|
-
const
|
|
3227
|
+
const syncIssues = [];
|
|
2392
3228
|
const asyncPromises = [];
|
|
2393
3229
|
for (const v of this.validators) try {
|
|
2394
3230
|
const r = v(value, abort.signal);
|
|
2395
3231
|
if (r instanceof Promise) asyncPromises.push(r);
|
|
2396
|
-
else
|
|
3232
|
+
else appendIssues(syncIssues, r);
|
|
2397
3233
|
} catch (err) {
|
|
2398
3234
|
try {
|
|
2399
3235
|
this.onValidatorError?.(err);
|
|
2400
3236
|
} catch {}
|
|
2401
|
-
|
|
3237
|
+
syncIssues.push({
|
|
3238
|
+
path: [],
|
|
3239
|
+
message: "Validation failed"
|
|
3240
|
+
});
|
|
2402
3241
|
}
|
|
2403
|
-
if (
|
|
3242
|
+
if (syncIssues.length > 0) {
|
|
2404
3243
|
batch(() => {
|
|
2405
|
-
this.topLevelErrors
|
|
3244
|
+
this.lastFormErrorTargets = routeFormIssues(this, syncIssues, this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2406
3245
|
this.topLevelValidating$.set(false);
|
|
2407
3246
|
});
|
|
2408
3247
|
return;
|
|
2409
3248
|
}
|
|
2410
3249
|
if (asyncPromises.length === 0) {
|
|
2411
3250
|
batch(() => {
|
|
2412
|
-
this.
|
|
3251
|
+
this.lastFormErrorTargets = routeFormIssues(this, [], this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2413
3252
|
this.topLevelValidating$.set(false);
|
|
2414
3253
|
});
|
|
2415
3254
|
return;
|
|
2416
3255
|
}
|
|
2417
3256
|
batch(() => {
|
|
2418
|
-
this.
|
|
3257
|
+
this.lastFormErrorTargets = routeFormIssues(this, [], this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2419
3258
|
this.topLevelValidating$.set(true);
|
|
2420
3259
|
});
|
|
2421
3260
|
Promise.allSettled(asyncPromises).then((results) => {
|
|
2422
3261
|
if (myId !== this.currentValidatorRun || this.disposed) return;
|
|
2423
|
-
const
|
|
2424
|
-
for (const r of results) if (r.status === "fulfilled"
|
|
3262
|
+
const issues = [];
|
|
3263
|
+
for (const r of results) if (r.status === "fulfilled") appendIssues(issues, r.value);
|
|
2425
3264
|
batch(() => {
|
|
2426
|
-
this.topLevelErrors
|
|
3265
|
+
this.lastFormErrorTargets = routeFormIssues(this, issues, this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2427
3266
|
this.topLevelValidating$.set(false);
|
|
2428
3267
|
});
|
|
2429
3268
|
});
|
|
2430
3269
|
}
|
|
2431
3270
|
};
|
|
3271
|
+
/**
|
|
3272
|
+
* Split a path string into segments, accepting both dot syntax (`users.0.name`)
|
|
3273
|
+
* and the bracket syntax `walkErrors` emits for array items (`users[0].name`).
|
|
3274
|
+
* Returns `null` if the path is malformed (unclosed bracket, empty bracket,
|
|
3275
|
+
* non-numeric bracket index).
|
|
3276
|
+
*/
|
|
3277
|
+
function splitPath(path) {
|
|
3278
|
+
const out = [];
|
|
3279
|
+
let current = "";
|
|
3280
|
+
for (let i = 0; i < path.length; i++) {
|
|
3281
|
+
const ch = path[i];
|
|
3282
|
+
if (ch === ".") {
|
|
3283
|
+
if (current !== "") {
|
|
3284
|
+
out.push(current);
|
|
3285
|
+
current = "";
|
|
3286
|
+
}
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
3289
|
+
if (ch === "[") {
|
|
3290
|
+
if (current !== "") {
|
|
3291
|
+
out.push(current);
|
|
3292
|
+
current = "";
|
|
3293
|
+
}
|
|
3294
|
+
const close = path.indexOf("]", i + 1);
|
|
3295
|
+
if (close === -1) return null;
|
|
3296
|
+
const idx = path.slice(i + 1, close);
|
|
3297
|
+
if (idx === "" || !/^\d+$/.test(idx)) return null;
|
|
3298
|
+
out.push(idx);
|
|
3299
|
+
i = close;
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
if (ch === "]") return null;
|
|
3303
|
+
current += ch;
|
|
3304
|
+
}
|
|
3305
|
+
if (current !== "") out.push(current);
|
|
3306
|
+
return out;
|
|
3307
|
+
}
|
|
3308
|
+
function collectDirtyFields(fields, prefix, out) {
|
|
3309
|
+
for (const [k, child] of Object.entries(fields)) {
|
|
3310
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
3311
|
+
if (isForm(child)) collectDirtyFields(child.fields, path, out);
|
|
3312
|
+
else if (isFieldArray(child)) child.items.value.forEach((item, idx) => {
|
|
3313
|
+
const itemPath = `${path}[${idx}]`;
|
|
3314
|
+
if (isForm(item)) collectDirtyFields(item.fields, itemPath, out);
|
|
3315
|
+
else if (item.isDirty.value) out.push(itemPath);
|
|
3316
|
+
});
|
|
3317
|
+
else if (child.isDirty.value) out.push(path);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
2432
3320
|
function walkErrors(fields, prefix, out) {
|
|
2433
3321
|
for (const [k, child] of Object.entries(fields)) {
|
|
2434
3322
|
const path = prefix ? `${prefix}.${k}` : k;
|
|
@@ -2482,9 +3370,28 @@ var FieldArrayImpl = class {
|
|
|
2482
3370
|
touched;
|
|
2483
3371
|
isValidating;
|
|
2484
3372
|
items$;
|
|
3373
|
+
/**
|
|
3374
|
+
* Structural dirtiness — flipped by `add`/`insert`/`remove`/`move`/`clear`.
|
|
3375
|
+
* Item-level `isDirty` alone misses these, so a reactive `initial` + the
|
|
3376
|
+
* default `resetOnInitialChange: 'when-clean'` would re-seat the array on a
|
|
3377
|
+
* background refetch and delete rows the user just added (T5.1). Reset by
|
|
3378
|
+
* `reset()` and by an initial-driven re-anchor (`replaceInitialItems`).
|
|
3379
|
+
*/
|
|
3380
|
+
structurallyDirty$ = signal(false);
|
|
2485
3381
|
topLevelErrors$ = signal([]);
|
|
2486
|
-
|
|
3382
|
+
/** Errors routed to this array by an ancestor form-level validator (T5.2) —
|
|
3383
|
+
* merged into `topLevelErrors` beside the array's own validator output. */
|
|
3384
|
+
parentFormErrors$ = signal([]);
|
|
3385
|
+
topLevelErrors = computed(() => {
|
|
3386
|
+
const own = this.topLevelErrors$.value;
|
|
3387
|
+
const parent = this.parentFormErrors$.value;
|
|
3388
|
+
if (parent.length === 0) return own;
|
|
3389
|
+
if (own.length === 0) return parent;
|
|
3390
|
+
return [...own, ...parent];
|
|
3391
|
+
});
|
|
2487
3392
|
topLevelValidating$ = signal(false);
|
|
3393
|
+
/** Targets written by the last array-level run — cleared next run if absent. */
|
|
3394
|
+
lastFormErrorTargets = /* @__PURE__ */ new Set();
|
|
2488
3395
|
itemFactory;
|
|
2489
3396
|
initialItems = [];
|
|
2490
3397
|
validators;
|
|
@@ -2519,6 +3426,7 @@ var FieldArrayImpl = class {
|
|
|
2519
3426
|
return errs.length > 0 ? errs : void 0;
|
|
2520
3427
|
}));
|
|
2521
3428
|
this.isDirty = computed(() => {
|
|
3429
|
+
if (this.structurallyDirty$.value) return true;
|
|
2522
3430
|
for (const item of this.items$.value) if (item.isDirty.value) return true;
|
|
2523
3431
|
return false;
|
|
2524
3432
|
});
|
|
@@ -2532,7 +3440,7 @@ var FieldArrayImpl = class {
|
|
|
2532
3440
|
return false;
|
|
2533
3441
|
});
|
|
2534
3442
|
this.isValid = computed(() => {
|
|
2535
|
-
if (this.topLevelErrors
|
|
3443
|
+
if (this.topLevelErrors.value.length > 0) return false;
|
|
2536
3444
|
if (this.isValidating.value) return false;
|
|
2537
3445
|
for (const item of this.items$.value) if (!item.isValid.value) return false;
|
|
2538
3446
|
return true;
|
|
@@ -2542,10 +3450,20 @@ var FieldArrayImpl = class {
|
|
|
2542
3450
|
at(index) {
|
|
2543
3451
|
return this.items$.peek()[index];
|
|
2544
3452
|
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Internal — receive errors routed here by an ancestor form-level validator
|
|
3455
|
+
* (a `FormIssue` whose path resolved to this array). See `routeFormIssues`.
|
|
3456
|
+
*/
|
|
3457
|
+
setFormErrors(errors) {
|
|
3458
|
+
if (this.disposed) return;
|
|
3459
|
+
if (this.parentFormErrors$.peek().length === 0 && errors.length === 0) return;
|
|
3460
|
+
this.parentFormErrors$.set(errors.length === 0 ? [] : [...errors]);
|
|
3461
|
+
}
|
|
2545
3462
|
add(initial) {
|
|
2546
3463
|
if (this.disposed) return;
|
|
2547
3464
|
const item = this.itemFactory(initial);
|
|
2548
3465
|
this.items$.set([...this.items$.peek(), item]);
|
|
3466
|
+
this.structurallyDirty$.set(true);
|
|
2549
3467
|
}
|
|
2550
3468
|
insert(index, initial) {
|
|
2551
3469
|
if (this.disposed) return;
|
|
@@ -2553,6 +3471,7 @@ var FieldArrayImpl = class {
|
|
|
2553
3471
|
const next = [...this.items$.peek()];
|
|
2554
3472
|
next.splice(index, 0, item);
|
|
2555
3473
|
this.items$.set(next);
|
|
3474
|
+
this.structurallyDirty$.set(true);
|
|
2556
3475
|
}
|
|
2557
3476
|
remove(index) {
|
|
2558
3477
|
if (this.disposed) return;
|
|
@@ -2560,6 +3479,7 @@ var FieldArrayImpl = class {
|
|
|
2560
3479
|
const [removed] = next.splice(index, 1);
|
|
2561
3480
|
if (removed) removed.dispose?.();
|
|
2562
3481
|
this.items$.set(next);
|
|
3482
|
+
this.structurallyDirty$.set(true);
|
|
2563
3483
|
}
|
|
2564
3484
|
move(from, to) {
|
|
2565
3485
|
if (this.disposed) return;
|
|
@@ -2567,11 +3487,13 @@ var FieldArrayImpl = class {
|
|
|
2567
3487
|
const [item] = next.splice(from, 1);
|
|
2568
3488
|
if (item) next.splice(to, 0, item);
|
|
2569
3489
|
this.items$.set(next);
|
|
3490
|
+
this.structurallyDirty$.set(true);
|
|
2570
3491
|
}
|
|
2571
3492
|
clear() {
|
|
2572
3493
|
if (this.disposed) return;
|
|
2573
3494
|
for (const item of this.items$.peek()) item.dispose?.();
|
|
2574
3495
|
this.items$.set([]);
|
|
3496
|
+
this.structurallyDirty$.set(true);
|
|
2575
3497
|
}
|
|
2576
3498
|
/**
|
|
2577
3499
|
* Internal — used by `Form.resetWithInitial` to re-anchor the array's
|
|
@@ -2581,6 +3503,7 @@ var FieldArrayImpl = class {
|
|
|
2581
3503
|
*/
|
|
2582
3504
|
replaceInitialItems(items) {
|
|
2583
3505
|
this.initialItems = [...items];
|
|
3506
|
+
this.structurallyDirty$.set(false);
|
|
2584
3507
|
}
|
|
2585
3508
|
reset() {
|
|
2586
3509
|
if (this.disposed) return;
|
|
@@ -2588,6 +3511,8 @@ var FieldArrayImpl = class {
|
|
|
2588
3511
|
this.clear();
|
|
2589
3512
|
for (const ini of this.initialItems) this.add(ini);
|
|
2590
3513
|
this.topLevelErrors$.set([]);
|
|
3514
|
+
this.parentFormErrors$.set([]);
|
|
3515
|
+
this.structurallyDirty$.set(false);
|
|
2591
3516
|
});
|
|
2592
3517
|
}
|
|
2593
3518
|
markAllTouched() {
|
|
@@ -2625,42 +3550,45 @@ var FieldArrayImpl = class {
|
|
|
2625
3550
|
const abort = new AbortController();
|
|
2626
3551
|
this.currentValidatorAbort = abort;
|
|
2627
3552
|
const myId = ++this.currentValidatorRun;
|
|
2628
|
-
const
|
|
3553
|
+
const syncIssues = [];
|
|
2629
3554
|
const asyncPromises = [];
|
|
2630
3555
|
for (const v of this.validators) try {
|
|
2631
3556
|
const r = v(value, abort.signal);
|
|
2632
3557
|
if (r instanceof Promise) asyncPromises.push(r);
|
|
2633
|
-
else
|
|
3558
|
+
else appendIssues(syncIssues, r);
|
|
2634
3559
|
} catch (err) {
|
|
2635
3560
|
try {
|
|
2636
3561
|
this.onValidatorError?.(err);
|
|
2637
3562
|
} catch {}
|
|
2638
|
-
|
|
3563
|
+
syncIssues.push({
|
|
3564
|
+
path: [],
|
|
3565
|
+
message: "Validation failed"
|
|
3566
|
+
});
|
|
2639
3567
|
}
|
|
2640
|
-
if (
|
|
3568
|
+
if (syncIssues.length > 0) {
|
|
2641
3569
|
batch(() => {
|
|
2642
|
-
this.topLevelErrors
|
|
3570
|
+
this.lastFormErrorTargets = routeFormIssues(this, syncIssues, this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2643
3571
|
this.topLevelValidating$.set(false);
|
|
2644
3572
|
});
|
|
2645
3573
|
return;
|
|
2646
3574
|
}
|
|
2647
3575
|
if (asyncPromises.length === 0) {
|
|
2648
3576
|
batch(() => {
|
|
2649
|
-
this.
|
|
3577
|
+
this.lastFormErrorTargets = routeFormIssues(this, [], this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2650
3578
|
this.topLevelValidating$.set(false);
|
|
2651
3579
|
});
|
|
2652
3580
|
return;
|
|
2653
3581
|
}
|
|
2654
3582
|
batch(() => {
|
|
2655
|
-
this.
|
|
3583
|
+
this.lastFormErrorTargets = routeFormIssues(this, [], this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2656
3584
|
this.topLevelValidating$.set(true);
|
|
2657
3585
|
});
|
|
2658
3586
|
Promise.allSettled(asyncPromises).then((results) => {
|
|
2659
3587
|
if (myId !== this.currentValidatorRun || this.disposed) return;
|
|
2660
|
-
const
|
|
2661
|
-
for (const r of results) if (r.status === "fulfilled"
|
|
3588
|
+
const issues = [];
|
|
3589
|
+
for (const r of results) if (r.status === "fulfilled") appendIssues(issues, r.value);
|
|
2662
3590
|
batch(() => {
|
|
2663
|
-
this.topLevelErrors
|
|
3591
|
+
this.lastFormErrorTargets = routeFormIssues(this, issues, this.topLevelErrors$, this.lastFormErrorTargets);
|
|
2664
3592
|
this.topLevelValidating$.set(false);
|
|
2665
3593
|
});
|
|
2666
3594
|
});
|
|
@@ -2797,6 +3725,9 @@ var LocalCacheImpl = class {
|
|
|
2797
3725
|
get hasPendingMutations() {
|
|
2798
3726
|
return this.entry.hasPendingMutations;
|
|
2799
3727
|
}
|
|
3728
|
+
get isPaused() {
|
|
3729
|
+
return this.entry.isPaused;
|
|
3730
|
+
}
|
|
2800
3731
|
refetch = () => this.entry.refetch();
|
|
2801
3732
|
reset = () => this.entry.reset();
|
|
2802
3733
|
firstValue = () => this.entry.firstValue();
|
|
@@ -2872,6 +3803,7 @@ var MutationImpl = class {
|
|
|
2872
3803
|
data = signal(void 0);
|
|
2873
3804
|
error = signal(void 0);
|
|
2874
3805
|
isPending = signal(false);
|
|
3806
|
+
status = signal("idle");
|
|
2875
3807
|
lastVariables = signal(void 0);
|
|
2876
3808
|
inflight = /* @__PURE__ */ new Set();
|
|
2877
3809
|
serialQueue = [];
|
|
@@ -2940,10 +3872,11 @@ var MutationImpl = class {
|
|
|
2940
3872
|
const raw = this.spec.onMutate?.(vars) ?? void 0;
|
|
2941
3873
|
snapshot = raw === void 0 ? void 0 : this.wrapSnapshot(raw);
|
|
2942
3874
|
} catch (err) {
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3875
|
+
this.error.set(err);
|
|
3876
|
+
this.status.set("error");
|
|
3877
|
+
this.safeCall(() => this.spec.onError?.(err, vars, void 0), "mutation");
|
|
3878
|
+
this.safeCall(() => this.spec.onSettled?.(void 0, err, vars), "mutation");
|
|
3879
|
+
throw err;
|
|
2947
3880
|
}
|
|
2948
3881
|
const handle = {
|
|
2949
3882
|
abort,
|
|
@@ -2953,6 +3886,7 @@ var MutationImpl = class {
|
|
|
2953
3886
|
this.inflightCounter?.update((n) => n + 1);
|
|
2954
3887
|
batch(() => {
|
|
2955
3888
|
this.isPending.set(true);
|
|
3889
|
+
this.status.set("pending");
|
|
2956
3890
|
this.lastVariables.set(vars);
|
|
2957
3891
|
});
|
|
2958
3892
|
const runId = this.isPersistable ? makeRunId() : "";
|
|
@@ -2984,6 +3918,7 @@ var MutationImpl = class {
|
|
|
2984
3918
|
batch(() => {
|
|
2985
3919
|
this.data.set(result);
|
|
2986
3920
|
this.error.set(void 0);
|
|
3921
|
+
this.status.set("success");
|
|
2987
3922
|
});
|
|
2988
3923
|
this.safeCall(() => this.spec.onSuccess?.(result, vars), "mutation");
|
|
2989
3924
|
snapshot?.finalize();
|
|
@@ -3005,6 +3940,7 @@ var MutationImpl = class {
|
|
|
3005
3940
|
throw err;
|
|
3006
3941
|
}
|
|
3007
3942
|
this.error.set(err);
|
|
3943
|
+
this.status.set("error");
|
|
3008
3944
|
this.safeCall(() => this.spec.onError?.(err, vars, snapshot), "mutation");
|
|
3009
3945
|
snapshot?.rollback();
|
|
3010
3946
|
this.safeCall(() => this.spec.onSettled?.(void 0, err, vars), "mutation");
|
|
@@ -3084,6 +4020,7 @@ var MutationImpl = class {
|
|
|
3084
4020
|
this.error.set(void 0);
|
|
3085
4021
|
this.lastVariables.set(void 0);
|
|
3086
4022
|
this.isPending.set(false);
|
|
4023
|
+
this.status.set("idle");
|
|
3087
4024
|
});
|
|
3088
4025
|
}
|
|
3089
4026
|
dispose() {
|
|
@@ -3157,6 +4094,7 @@ var SubscriptionImpl = class {
|
|
|
3157
4094
|
isStale;
|
|
3158
4095
|
lastUpdatedAt;
|
|
3159
4096
|
hasPendingMutations;
|
|
4097
|
+
isPaused;
|
|
3160
4098
|
constructor(keepPreviousData, select) {
|
|
3161
4099
|
this.keepPreviousData = keepPreviousData;
|
|
3162
4100
|
this.select = select;
|
|
@@ -3181,6 +4119,7 @@ var SubscriptionImpl = class {
|
|
|
3181
4119
|
this.isStale = computed(() => this.current$.value?.entry.isStale.value ?? true);
|
|
3182
4120
|
this.lastUpdatedAt = computed(() => this.current$.value?.entry.lastUpdatedAt.value);
|
|
3183
4121
|
this.hasPendingMutations = computed(() => this.current$.value?.entry.hasPendingMutations.value ?? false);
|
|
4122
|
+
this.isPaused = computed(() => this.current$.value?.entry.isPaused.value ?? false);
|
|
3184
4123
|
}
|
|
3185
4124
|
attach(entry) {
|
|
3186
4125
|
const prev = this.current$.peek();
|
|
@@ -3197,11 +4136,17 @@ var SubscriptionImpl = class {
|
|
|
3197
4136
|
refetch = () => {
|
|
3198
4137
|
const cur = this.current$.peek();
|
|
3199
4138
|
if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
|
|
3200
|
-
return cur.entry.refetch().then((v) => this.project(v))
|
|
4139
|
+
return cur.entry.refetch().then((v) => this.project(v), (err) => {
|
|
4140
|
+
if (isAbortError(err)) return this.firstValue();
|
|
4141
|
+
throw err;
|
|
4142
|
+
});
|
|
3201
4143
|
};
|
|
3202
4144
|
reset = () => {
|
|
3203
4145
|
this.current$.peek()?.entry.reset();
|
|
3204
4146
|
};
|
|
4147
|
+
cancel = () => {
|
|
4148
|
+
this.current$.peek()?.entry.cancel();
|
|
4149
|
+
};
|
|
3205
4150
|
firstValue = () => {
|
|
3206
4151
|
const cur = this.current$.peek();
|
|
3207
4152
|
if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
|
|
@@ -3229,8 +4174,10 @@ function createUse(client, query, keyOrOptions) {
|
|
|
3229
4174
|
let currentEntry = null;
|
|
3230
4175
|
let suspended = false;
|
|
3231
4176
|
const effectDispose = effect(() => {
|
|
4177
|
+
const isEnabled = enabledFn ? enabledFn() : true;
|
|
4178
|
+
const args = isEnabled ? keyFn ? keyFn() : [] : void 0;
|
|
3232
4179
|
if (suspended) return;
|
|
3233
|
-
if (!
|
|
4180
|
+
if (!isEnabled) {
|
|
3234
4181
|
untracked(() => {
|
|
3235
4182
|
if (currentEntry) {
|
|
3236
4183
|
currentEntry.release();
|
|
@@ -3240,7 +4187,6 @@ function createUse(client, query, keyOrOptions) {
|
|
|
3240
4187
|
});
|
|
3241
4188
|
return;
|
|
3242
4189
|
}
|
|
3243
|
-
const args = keyFn ? keyFn() : [];
|
|
3244
4190
|
untracked(() => {
|
|
3245
4191
|
const entry = client.bindEntry(query, args);
|
|
3246
4192
|
if (currentEntry === entry) return;
|
|
@@ -3301,6 +4247,7 @@ var InfiniteSubscriptionImpl = class {
|
|
|
3301
4247
|
isStale;
|
|
3302
4248
|
lastUpdatedAt;
|
|
3303
4249
|
hasPendingMutations;
|
|
4250
|
+
isPaused;
|
|
3304
4251
|
hasNextPage;
|
|
3305
4252
|
hasPreviousPage;
|
|
3306
4253
|
isFetchingNextPage;
|
|
@@ -3337,6 +4284,7 @@ var InfiniteSubscriptionImpl = class {
|
|
|
3337
4284
|
this.isStale = computed(() => this.current$.value?.entry.isStale.value ?? true);
|
|
3338
4285
|
this.lastUpdatedAt = computed(() => this.current$.value?.entry.lastUpdatedAt.value);
|
|
3339
4286
|
this.hasPendingMutations = computed(() => this.current$.value?.entry.hasPendingMutations.value ?? false);
|
|
4287
|
+
this.isPaused = computed(() => this.current$.value?.entry.isPaused.value ?? false);
|
|
3340
4288
|
this.hasNextPage = computed(() => this.current$.value?.entry.hasNextPage.value ?? false);
|
|
3341
4289
|
this.hasPreviousPage = computed(() => this.current$.value?.entry.hasPreviousPage.value ?? false);
|
|
3342
4290
|
this.isFetchingNextPage = computed(() => this.current$.value?.entry.isFetchingNextPage.value ?? false);
|
|
@@ -3357,11 +4305,17 @@ var InfiniteSubscriptionImpl = class {
|
|
|
3357
4305
|
refetch = () => {
|
|
3358
4306
|
const cur = this.current$.peek();
|
|
3359
4307
|
if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
|
|
3360
|
-
return cur.entry.refetch().then(() => cur.entry.pages.peek())
|
|
4308
|
+
return cur.entry.refetch().then(() => cur.entry.pages.peek(), (err) => {
|
|
4309
|
+
if (isAbortError(err)) return this.firstValue();
|
|
4310
|
+
throw err;
|
|
4311
|
+
});
|
|
3361
4312
|
};
|
|
3362
4313
|
reset = () => {
|
|
3363
4314
|
this.current$.peek()?.entry.reset();
|
|
3364
4315
|
};
|
|
4316
|
+
cancel = () => {
|
|
4317
|
+
this.current$.peek()?.entry.cancel();
|
|
4318
|
+
};
|
|
3365
4319
|
firstValue = () => {
|
|
3366
4320
|
const cur = this.current$.peek();
|
|
3367
4321
|
if (!cur) return Promise.reject(/* @__PURE__ */ new Error("[olas] no active subscription"));
|
|
@@ -3387,8 +4341,10 @@ function createInfiniteUse(client, query, keyOrOptions) {
|
|
|
3387
4341
|
let currentEntry = null;
|
|
3388
4342
|
let suspended = false;
|
|
3389
4343
|
const effectDispose = effect(() => {
|
|
4344
|
+
const isEnabled = enabledFn ? enabledFn() : true;
|
|
4345
|
+
const args = isEnabled ? keyFn ? keyFn() : [] : void 0;
|
|
3390
4346
|
if (suspended) return;
|
|
3391
|
-
if (!
|
|
4347
|
+
if (!isEnabled) {
|
|
3392
4348
|
untracked(() => {
|
|
3393
4349
|
if (currentEntry) {
|
|
3394
4350
|
currentEntry.release();
|
|
@@ -3398,7 +4354,6 @@ function createInfiniteUse(client, query, keyOrOptions) {
|
|
|
3398
4354
|
});
|
|
3399
4355
|
return;
|
|
3400
4356
|
}
|
|
3401
|
-
const args = keyFn ? keyFn() : [];
|
|
3402
4357
|
untracked(() => {
|
|
3403
4358
|
const entry = client.bindInfiniteEntry(query, args);
|
|
3404
4359
|
if (currentEntry === entry) return;
|
|
@@ -3447,16 +4402,100 @@ function createInfiniteUse(client, query, keyOrOptions) {
|
|
|
3447
4402
|
}
|
|
3448
4403
|
//#endregion
|
|
3449
4404
|
//#region src/controller/instance.ts
|
|
4405
|
+
/**
|
|
4406
|
+
* Append-and-unlink container for `LifecycleEntry`s. Replaces the historical
|
|
4407
|
+
* `LifecycleEntry[]` to make per-entry removal O(1) — important for
|
|
4408
|
+
* controllers that churn entries (`ctx.collection` reconcile, `lazyChild`
|
|
4409
|
+
* loop, `attach` short-lived children).
|
|
4410
|
+
*
|
|
4411
|
+
* Iteration order is insertion order for `forward()` and reverse-insertion
|
|
4412
|
+
* for `reverse()`. Iteration is safe against `push` during traversal (the
|
|
4413
|
+
* generator captures `next`/`prev` *before* yielding) but not against
|
|
4414
|
+
* unlinking the current node from inside the visitor — visitors must not
|
|
4415
|
+
* call `unlink` on the entry they're currently inspecting.
|
|
4416
|
+
*/
|
|
4417
|
+
var LifecycleList = class {
|
|
4418
|
+
head = null;
|
|
4419
|
+
tail = null;
|
|
4420
|
+
_size = 0;
|
|
4421
|
+
get size() {
|
|
4422
|
+
return this._size;
|
|
4423
|
+
}
|
|
4424
|
+
push(entry) {
|
|
4425
|
+
const node = {
|
|
4426
|
+
entry,
|
|
4427
|
+
prev: this.tail,
|
|
4428
|
+
next: null,
|
|
4429
|
+
unlinked: false
|
|
4430
|
+
};
|
|
4431
|
+
if (this.tail !== null) this.tail.next = node;
|
|
4432
|
+
else this.head = node;
|
|
4433
|
+
this.tail = node;
|
|
4434
|
+
this._size += 1;
|
|
4435
|
+
return node;
|
|
4436
|
+
}
|
|
4437
|
+
unlink(node) {
|
|
4438
|
+
if (node.unlinked) return;
|
|
4439
|
+
node.unlinked = true;
|
|
4440
|
+
if (node.prev !== null) node.prev.next = node.next;
|
|
4441
|
+
else this.head = node.next;
|
|
4442
|
+
if (node.next !== null) node.next.prev = node.prev;
|
|
4443
|
+
else this.tail = node.prev;
|
|
4444
|
+
this._size -= 1;
|
|
4445
|
+
}
|
|
4446
|
+
clear() {
|
|
4447
|
+
this.head = null;
|
|
4448
|
+
this.tail = null;
|
|
4449
|
+
this._size = 0;
|
|
4450
|
+
}
|
|
4451
|
+
/** Yield entries in insertion order. */
|
|
4452
|
+
*forward() {
|
|
4453
|
+
let n = this.head;
|
|
4454
|
+
while (n !== null) {
|
|
4455
|
+
const next = n.next;
|
|
4456
|
+
yield n.entry;
|
|
4457
|
+
n = next;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
/** Yield entries in reverse-insertion order — used by dispose / suspend. */
|
|
4461
|
+
*reverse() {
|
|
4462
|
+
let n = this.tail;
|
|
4463
|
+
while (n !== null) {
|
|
4464
|
+
const prev = n.prev;
|
|
4465
|
+
yield n.entry;
|
|
4466
|
+
n = prev;
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
};
|
|
3450
4470
|
var ControllerInstance = class ControllerInstance {
|
|
3451
4471
|
path;
|
|
3452
4472
|
deps;
|
|
3453
4473
|
state = "constructing";
|
|
3454
|
-
entries =
|
|
4474
|
+
entries = new LifecycleList();
|
|
3455
4475
|
rootShared;
|
|
3456
4476
|
parent;
|
|
3457
4477
|
childCounter = 0;
|
|
3458
4478
|
/** Scope values provided on this instance, keyed by `Scope.__id`. */
|
|
3459
4479
|
scopes = null;
|
|
4480
|
+
/**
|
|
4481
|
+
* Memoized result of `ctx.inject(scope)` per scope id, stamped with the
|
|
4482
|
+
* `scopesVersion` the lookup observed. A bump invalidates every cache
|
|
4483
|
+
* entry implicitly — the next `inject(scope)` finds a stale version
|
|
4484
|
+
* stamp and re-walks. Provide is rare; reads dominate.
|
|
4485
|
+
*/
|
|
4486
|
+
injectCache = null;
|
|
4487
|
+
/**
|
|
4488
|
+
* Pre-seed scopes from outside the factory — used by `createRoot`'s
|
|
4489
|
+
* `scopes:` option so an adapter (e.g. `@kontsedal/olas-router-tanstack`)
|
|
4490
|
+
* can publish cross-cutting values without forcing the user to call
|
|
4491
|
+
* `ctx.provide(...)` in their root controller. Idempotent per scope id:
|
|
4492
|
+
* later calls override.
|
|
4493
|
+
*/
|
|
4494
|
+
seedScopes(bindings) {
|
|
4495
|
+
if (bindings.length === 0) return;
|
|
4496
|
+
if (this.scopes === null) this.scopes = /* @__PURE__ */ new Map();
|
|
4497
|
+
for (const [scope, value] of bindings) this.scopes.set(scope.__id, value);
|
|
4498
|
+
}
|
|
3460
4499
|
constructor(parent, rootShared, pathSegment, deps) {
|
|
3461
4500
|
this.parent = parent;
|
|
3462
4501
|
this.rootShared = rootShared;
|
|
@@ -3480,33 +4519,31 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3480
4519
|
return api;
|
|
3481
4520
|
}
|
|
3482
4521
|
rollbackPartialConstruction() {
|
|
3483
|
-
for (
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
4522
|
+
for (const entry of this.entries.reverse()) try {
|
|
4523
|
+
this.disposeEntry(entry);
|
|
4524
|
+
} catch (err) {
|
|
4525
|
+
dispatchError(this.rootShared.onError, err, {
|
|
4526
|
+
kind: "effect",
|
|
4527
|
+
controllerPath: this.path
|
|
4528
|
+
});
|
|
3489
4529
|
}
|
|
3490
|
-
this.entries.
|
|
4530
|
+
this.entries.clear();
|
|
3491
4531
|
this.state = "disposed";
|
|
3492
4532
|
}
|
|
3493
4533
|
dispose() {
|
|
3494
4534
|
if (this.state === "disposed") return;
|
|
3495
4535
|
this.state = "disposed";
|
|
3496
|
-
for (
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
kind: "effect",
|
|
3504
|
-
controllerPath: this.path
|
|
3505
|
-
});
|
|
3506
|
-
}
|
|
4536
|
+
for (const entry of this.entries.reverse()) try {
|
|
4537
|
+
this.disposeEntry(entry);
|
|
4538
|
+
} catch (err) {
|
|
4539
|
+
dispatchError(this.rootShared.onError, err, {
|
|
4540
|
+
kind: "effect",
|
|
4541
|
+
controllerPath: this.path
|
|
4542
|
+
});
|
|
3507
4543
|
}
|
|
3508
|
-
this.entries.
|
|
4544
|
+
this.entries.clear();
|
|
3509
4545
|
this.scopes = null;
|
|
4546
|
+
this.injectCache = null;
|
|
3510
4547
|
}
|
|
3511
4548
|
disposeEntry(entry) {
|
|
3512
4549
|
switch (entry.kind) {
|
|
@@ -3533,49 +4570,49 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3533
4570
|
case "onResume": break;
|
|
3534
4571
|
}
|
|
3535
4572
|
}
|
|
4573
|
+
isSuspended() {
|
|
4574
|
+
return this.state === "suspended";
|
|
4575
|
+
}
|
|
3536
4576
|
suspend() {
|
|
3537
4577
|
if (this.state !== "active") return;
|
|
3538
4578
|
this.state = "suspended";
|
|
3539
|
-
for (
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
entry.fn();
|
|
3556
|
-
break;
|
|
3557
|
-
default: break;
|
|
3558
|
-
}
|
|
3559
|
-
} catch (err) {
|
|
3560
|
-
dispatchError(this.rootShared.onError, err, {
|
|
3561
|
-
kind: "effect",
|
|
3562
|
-
controllerPath: this.path
|
|
3563
|
-
});
|
|
4579
|
+
for (const entry of this.entries.reverse()) try {
|
|
4580
|
+
switch (entry.kind) {
|
|
4581
|
+
case "effect":
|
|
4582
|
+
entry.dispose?.();
|
|
4583
|
+
entry.dispose = null;
|
|
4584
|
+
break;
|
|
4585
|
+
case "subscription-cache":
|
|
4586
|
+
entry.suspend();
|
|
4587
|
+
break;
|
|
4588
|
+
case "child":
|
|
4589
|
+
entry.instance.suspend();
|
|
4590
|
+
break;
|
|
4591
|
+
case "onSuspend":
|
|
4592
|
+
entry.fn();
|
|
4593
|
+
break;
|
|
4594
|
+
default: break;
|
|
3564
4595
|
}
|
|
4596
|
+
} catch (err) {
|
|
4597
|
+
dispatchError(this.rootShared.onError, err, {
|
|
4598
|
+
kind: "effect",
|
|
4599
|
+
controllerPath: this.path
|
|
4600
|
+
});
|
|
3565
4601
|
}
|
|
3566
4602
|
}
|
|
3567
4603
|
resume() {
|
|
3568
4604
|
if (this.state !== "suspended") return;
|
|
3569
4605
|
this.state = "active";
|
|
3570
|
-
for (const entry of this.entries) try {
|
|
4606
|
+
for (const entry of this.entries.forward()) try {
|
|
3571
4607
|
switch (entry.kind) {
|
|
3572
4608
|
case "effect":
|
|
3573
|
-
entry.dispose = effect(entry.factory);
|
|
4609
|
+
if (entry.dispose === null) entry.dispose = effect(entry.factory);
|
|
3574
4610
|
break;
|
|
3575
4611
|
case "subscription-cache":
|
|
3576
4612
|
entry.resume();
|
|
3577
4613
|
break;
|
|
3578
4614
|
case "child":
|
|
4615
|
+
if (entry.explicitlySuspended) break;
|
|
3579
4616
|
entry.instance.resume();
|
|
3580
4617
|
break;
|
|
3581
4618
|
case "onResume":
|
|
@@ -3592,12 +4629,15 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3592
4629
|
}
|
|
3593
4630
|
buildCtx() {
|
|
3594
4631
|
const self = this;
|
|
4632
|
+
const assertLive = (method) => {
|
|
4633
|
+
if (self.isTerminal()) throw new Error(`[olas] ctx.${method}() called after the controller was disposed`);
|
|
4634
|
+
};
|
|
3595
4635
|
return {
|
|
3596
4636
|
get deps() {
|
|
3597
4637
|
return self.deps;
|
|
3598
4638
|
},
|
|
3599
4639
|
effect(fn) {
|
|
3600
|
-
|
|
4640
|
+
assertLive("effect");
|
|
3601
4641
|
const entry = {
|
|
3602
4642
|
kind: "effect",
|
|
3603
4643
|
factory: () => fn(),
|
|
@@ -3605,7 +4645,18 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3605
4645
|
};
|
|
3606
4646
|
const wrapped = () => {
|
|
3607
4647
|
try {
|
|
3608
|
-
|
|
4648
|
+
const cleanup = fn();
|
|
4649
|
+
if (typeof cleanup === "function") return () => {
|
|
4650
|
+
try {
|
|
4651
|
+
cleanup();
|
|
4652
|
+
} catch (err) {
|
|
4653
|
+
dispatchError(self.rootShared.onError, err, {
|
|
4654
|
+
kind: "effect",
|
|
4655
|
+
controllerPath: self.path
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
};
|
|
4659
|
+
return cleanup;
|
|
3609
4660
|
} catch (err) {
|
|
3610
4661
|
dispatchError(self.rootShared.onError, err, {
|
|
3611
4662
|
kind: "effect",
|
|
@@ -3619,6 +4670,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3619
4670
|
self.entries.push(entry);
|
|
3620
4671
|
},
|
|
3621
4672
|
cache(fetcher, options) {
|
|
4673
|
+
assertLive("cache");
|
|
3622
4674
|
const cache = createLocalCache(fetcher, options);
|
|
3623
4675
|
self.entries.push({
|
|
3624
4676
|
kind: "cleanup",
|
|
@@ -3627,6 +4679,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3627
4679
|
return cache;
|
|
3628
4680
|
},
|
|
3629
4681
|
use(query, keyOrOptions) {
|
|
4682
|
+
assertLive("use");
|
|
3630
4683
|
if (query.__olas === "infiniteQuery") {
|
|
3631
4684
|
const handle = createInfiniteUse(self.rootShared.queryClient, query, keyOrOptions);
|
|
3632
4685
|
self.entries.push({
|
|
@@ -3647,6 +4700,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3647
4700
|
return handle.subscription;
|
|
3648
4701
|
},
|
|
3649
4702
|
mutation(spec) {
|
|
4703
|
+
assertLive("mutation");
|
|
3650
4704
|
const queryClient = self.rootShared.queryClient;
|
|
3651
4705
|
const m = createMutation(spec, self.rootShared.onError, self.path, queryClient.mutationsInflight$, self.rootShared.devtools, spec.persist === true ? {
|
|
3652
4706
|
emitEnqueue: (ev) => queryClient.emitMutationEnqueue(ev),
|
|
@@ -3659,6 +4713,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3659
4713
|
return m;
|
|
3660
4714
|
},
|
|
3661
4715
|
emitter() {
|
|
4716
|
+
assertLive("emitter");
|
|
3662
4717
|
const e = createEmitter({ onError: (err) => {
|
|
3663
4718
|
dispatchError(self.rootShared.onError, err, {
|
|
3664
4719
|
kind: "emitter",
|
|
@@ -3671,13 +4726,19 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3671
4726
|
});
|
|
3672
4727
|
return e;
|
|
3673
4728
|
},
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4729
|
+
signal,
|
|
4730
|
+
computed,
|
|
4731
|
+
field(initial, validators, options) {
|
|
4732
|
+
assertLive("field");
|
|
4733
|
+
const f = createField(initial, validators, {
|
|
4734
|
+
onValidatorError: (err) => {
|
|
4735
|
+
dispatchError(self.rootShared.onError, err, {
|
|
4736
|
+
kind: "effect",
|
|
4737
|
+
controllerPath: self.path
|
|
4738
|
+
});
|
|
4739
|
+
},
|
|
4740
|
+
validateOn: options?.validateOn
|
|
4741
|
+
});
|
|
3681
4742
|
self.entries.push({
|
|
3682
4743
|
kind: "cleanup",
|
|
3683
4744
|
dispose: () => f.dispose()
|
|
@@ -3690,6 +4751,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3690
4751
|
return f;
|
|
3691
4752
|
},
|
|
3692
4753
|
form(schema, options) {
|
|
4754
|
+
assertLive("form");
|
|
3693
4755
|
const reporter = (err) => {
|
|
3694
4756
|
dispatchError(self.rootShared.onError, err, {
|
|
3695
4757
|
kind: "effect",
|
|
@@ -3710,6 +4772,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3710
4772
|
return f;
|
|
3711
4773
|
},
|
|
3712
4774
|
fieldArray(itemFactory, options) {
|
|
4775
|
+
assertLive("fieldArray");
|
|
3713
4776
|
const reporter = (err) => {
|
|
3714
4777
|
dispatchError(self.rootShared.onError, err, {
|
|
3715
4778
|
kind: "effect",
|
|
@@ -3732,19 +4795,44 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3732
4795
|
provide(scope, value) {
|
|
3733
4796
|
if (self.scopes === null) self.scopes = /* @__PURE__ */ new Map();
|
|
3734
4797
|
self.scopes.set(scope.__id, value);
|
|
4798
|
+
self.rootShared.scopesVersion.value += 1;
|
|
3735
4799
|
},
|
|
3736
4800
|
inject(scope) {
|
|
4801
|
+
const version = self.rootShared.scopesVersion.value;
|
|
4802
|
+
const cache = self.injectCache;
|
|
4803
|
+
if (cache !== null) {
|
|
4804
|
+
const cached = cache.get(scope.__id);
|
|
4805
|
+
if (cached !== void 0 && cached.version === version) return cached.value;
|
|
4806
|
+
}
|
|
4807
|
+
const ensureMemo = () => {
|
|
4808
|
+
if (self.injectCache === null) self.injectCache = /* @__PURE__ */ new Map();
|
|
4809
|
+
return self.injectCache;
|
|
4810
|
+
};
|
|
3737
4811
|
let node = self;
|
|
3738
4812
|
while (node !== null) {
|
|
3739
4813
|
const map = node.scopes;
|
|
3740
|
-
if (map?.has(scope.__id))
|
|
4814
|
+
if (map?.has(scope.__id)) {
|
|
4815
|
+
const value = map.get(scope.__id);
|
|
4816
|
+
ensureMemo().set(scope.__id, {
|
|
4817
|
+
value,
|
|
4818
|
+
version
|
|
4819
|
+
});
|
|
4820
|
+
return value;
|
|
4821
|
+
}
|
|
3741
4822
|
node = node.parent;
|
|
3742
4823
|
}
|
|
3743
|
-
if (scope.hasDefault)
|
|
4824
|
+
if (scope.hasDefault) {
|
|
4825
|
+
ensureMemo().set(scope.__id, {
|
|
4826
|
+
value: scope.default,
|
|
4827
|
+
version
|
|
4828
|
+
});
|
|
4829
|
+
return scope.default;
|
|
4830
|
+
}
|
|
3744
4831
|
const label = scope.name ?? scope.__id.description ?? "unnamed";
|
|
3745
4832
|
throw new Error(`[olas] ctx.inject(): no provider for scope '${label}' and no default. Provide it on an ancestor via ctx.provide(${label}, ...) or pass a default to defineScope.`);
|
|
3746
4833
|
},
|
|
3747
4834
|
on(emitter, handler) {
|
|
4835
|
+
assertLive("on");
|
|
3748
4836
|
const wrapped = (value) => {
|
|
3749
4837
|
try {
|
|
3750
4838
|
handler(value);
|
|
@@ -3762,6 +4850,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3762
4850
|
});
|
|
3763
4851
|
},
|
|
3764
4852
|
child(def, props, options) {
|
|
4853
|
+
assertLive("child");
|
|
3765
4854
|
const segment = self.makeChildSegment(getFactory(def), getName(def));
|
|
3766
4855
|
const override = options?.deps;
|
|
3767
4856
|
const childDeps = override !== void 0 ? {
|
|
@@ -3777,6 +4866,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3777
4866
|
return api;
|
|
3778
4867
|
},
|
|
3779
4868
|
attach(def, props, options) {
|
|
4869
|
+
assertLive("attach");
|
|
3780
4870
|
const segment = self.makeChildSegment(getFactory(def), getName(def));
|
|
3781
4871
|
const override = options?.deps;
|
|
3782
4872
|
const childDeps = override !== void 0 ? {
|
|
@@ -3787,17 +4877,17 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3787
4877
|
const api = childInstance.construct(getFactory(def), props);
|
|
3788
4878
|
const entry = {
|
|
3789
4879
|
kind: "child",
|
|
3790
|
-
instance: childInstance
|
|
4880
|
+
instance: childInstance,
|
|
4881
|
+
explicitlySuspended: false
|
|
3791
4882
|
};
|
|
3792
|
-
self.entries.push(entry);
|
|
4883
|
+
const node = self.entries.push(entry);
|
|
3793
4884
|
let disposed = false;
|
|
3794
4885
|
return {
|
|
3795
4886
|
api,
|
|
3796
4887
|
dispose: () => {
|
|
3797
4888
|
if (disposed) return;
|
|
3798
4889
|
disposed = true;
|
|
3799
|
-
|
|
3800
|
-
if (idx >= 0) self.entries.splice(idx, 1);
|
|
4890
|
+
self.entries.unlink(node);
|
|
3801
4891
|
try {
|
|
3802
4892
|
childInstance.dispose();
|
|
3803
4893
|
} catch (err) {
|
|
@@ -3809,6 +4899,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3809
4899
|
},
|
|
3810
4900
|
suspend: () => {
|
|
3811
4901
|
if (disposed) return;
|
|
4902
|
+
entry.explicitlySuspended = true;
|
|
3812
4903
|
try {
|
|
3813
4904
|
childInstance.suspend();
|
|
3814
4905
|
} catch (err) {
|
|
@@ -3820,6 +4911,8 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3820
4911
|
},
|
|
3821
4912
|
resume: () => {
|
|
3822
4913
|
if (disposed) return;
|
|
4914
|
+
entry.explicitlySuspended = false;
|
|
4915
|
+
if (self.isSuspended()) return;
|
|
3823
4916
|
try {
|
|
3824
4917
|
childInstance.resume();
|
|
3825
4918
|
} catch (err) {
|
|
@@ -3832,6 +4925,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3832
4925
|
};
|
|
3833
4926
|
},
|
|
3834
4927
|
session(def, props, options) {
|
|
4928
|
+
assertLive("session");
|
|
3835
4929
|
const segment = self.makeChildSegment(getFactory(def), getName(def));
|
|
3836
4930
|
const override = options?.deps;
|
|
3837
4931
|
const childDeps = override !== void 0 ? {
|
|
@@ -3844,13 +4938,12 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3844
4938
|
kind: "child",
|
|
3845
4939
|
instance: childInstance
|
|
3846
4940
|
};
|
|
3847
|
-
self.entries.push(entry);
|
|
4941
|
+
const node = self.entries.push(entry);
|
|
3848
4942
|
let disposed = false;
|
|
3849
4943
|
const dispose = () => {
|
|
3850
4944
|
if (disposed) return;
|
|
3851
4945
|
disposed = true;
|
|
3852
|
-
|
|
3853
|
-
if (idx >= 0) self.entries.splice(idx, 1);
|
|
4946
|
+
self.entries.unlink(node);
|
|
3854
4947
|
try {
|
|
3855
4948
|
childInstance.dispose();
|
|
3856
4949
|
} catch (err) {
|
|
@@ -3863,6 +4956,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3863
4956
|
return [api, dispose];
|
|
3864
4957
|
},
|
|
3865
4958
|
collection(options) {
|
|
4959
|
+
assertLive("collection");
|
|
3866
4960
|
const childMap = /* @__PURE__ */ new Map();
|
|
3867
4961
|
const items$ = signal([]);
|
|
3868
4962
|
const size$ = computed(() => items$.value.length);
|
|
@@ -3903,8 +4997,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3903
4997
|
const info = childMap.get(key);
|
|
3904
4998
|
if (info === void 0) return;
|
|
3905
4999
|
childMap.delete(key);
|
|
3906
|
-
|
|
3907
|
-
if (idx >= 0) self.entries.splice(idx, 1);
|
|
5000
|
+
self.entries.unlink(info.node);
|
|
3908
5001
|
try {
|
|
3909
5002
|
info.instance.dispose();
|
|
3910
5003
|
} catch (err) {
|
|
@@ -3916,60 +5009,63 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3916
5009
|
};
|
|
3917
5010
|
const reconcile = () => {
|
|
3918
5011
|
const source = options.source.value;
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
const
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
const
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
const
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
5012
|
+
untracked(() => {
|
|
5013
|
+
const itemByKey = /* @__PURE__ */ new Map();
|
|
5014
|
+
for (const item of source) {
|
|
5015
|
+
const key = options.keyOf(item);
|
|
5016
|
+
if (itemByKey.has(key)) continue;
|
|
5017
|
+
itemByKey.set(key, item);
|
|
5018
|
+
}
|
|
5019
|
+
for (const key of [...childMap.keys()]) if (!itemByKey.has(key)) removeKey(key);
|
|
5020
|
+
for (const [key, item] of itemByKey) {
|
|
5021
|
+
const existing = childMap.get(key);
|
|
5022
|
+
if (existing !== void 0) {
|
|
5023
|
+
if (isFactoryForm) {
|
|
5024
|
+
if (options.factory(item).controller !== existing.def) {
|
|
5025
|
+
removeKey(key);
|
|
5026
|
+
const built = buildChild(item);
|
|
5027
|
+
if (built !== null) {
|
|
5028
|
+
const entry = {
|
|
5029
|
+
kind: "child",
|
|
5030
|
+
instance: built.instance
|
|
5031
|
+
};
|
|
5032
|
+
const node = self.entries.push(entry);
|
|
5033
|
+
childMap.set(key, {
|
|
5034
|
+
...built,
|
|
5035
|
+
node
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
3942
5038
|
}
|
|
3943
5039
|
}
|
|
5040
|
+
continue;
|
|
5041
|
+
}
|
|
5042
|
+
const built = buildChild(item);
|
|
5043
|
+
if (built !== null) {
|
|
5044
|
+
const entry = {
|
|
5045
|
+
kind: "child",
|
|
5046
|
+
instance: built.instance
|
|
5047
|
+
};
|
|
5048
|
+
const node = self.entries.push(entry);
|
|
5049
|
+
childMap.set(key, {
|
|
5050
|
+
...built,
|
|
5051
|
+
node
|
|
5052
|
+
});
|
|
3944
5053
|
}
|
|
3945
|
-
continue;
|
|
3946
5054
|
}
|
|
3947
|
-
const
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
5055
|
+
const next = [];
|
|
5056
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5057
|
+
for (const item of source) {
|
|
5058
|
+
const key = options.keyOf(item);
|
|
5059
|
+
if (seen.has(key)) continue;
|
|
5060
|
+
seen.add(key);
|
|
5061
|
+
const info = childMap.get(key);
|
|
5062
|
+
if (info !== void 0) next.push({
|
|
5063
|
+
key,
|
|
5064
|
+
api: info.api
|
|
3957
5065
|
});
|
|
3958
5066
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3962
|
-
for (const item of source) {
|
|
3963
|
-
const key = options.keyOf(item);
|
|
3964
|
-
if (seen.has(key)) continue;
|
|
3965
|
-
seen.add(key);
|
|
3966
|
-
const info = childMap.get(key);
|
|
3967
|
-
if (info !== void 0) next.push({
|
|
3968
|
-
key,
|
|
3969
|
-
api: info.api
|
|
3970
|
-
});
|
|
3971
|
-
}
|
|
3972
|
-
items$.set(next);
|
|
5067
|
+
items$.set(next);
|
|
5068
|
+
});
|
|
3973
5069
|
};
|
|
3974
5070
|
const wrapped = () => {
|
|
3975
5071
|
try {
|
|
@@ -3989,27 +5085,45 @@ var ControllerInstance = class ControllerInstance {
|
|
|
3989
5085
|
if (self.state !== "suspended") effectEntry.dispose = effect(wrapped);
|
|
3990
5086
|
self.entries.push(effectEntry);
|
|
3991
5087
|
return {
|
|
3992
|
-
items: items
|
|
3993
|
-
size: size
|
|
5088
|
+
items: readOnly(items$),
|
|
5089
|
+
size: readOnly(size$),
|
|
3994
5090
|
get: (key) => childMap.get(key)?.api,
|
|
3995
|
-
has: (key) => childMap.has(key)
|
|
5091
|
+
has: (key) => childMap.has(key),
|
|
5092
|
+
suspendItem: (key) => {
|
|
5093
|
+
const info = childMap.get(key);
|
|
5094
|
+
if (info === void 0) return;
|
|
5095
|
+
if (info.node.entry.kind === "child") info.node.entry.explicitlySuspended = true;
|
|
5096
|
+
info.instance.suspend();
|
|
5097
|
+
},
|
|
5098
|
+
resumeItem: (key) => {
|
|
5099
|
+
const info = childMap.get(key);
|
|
5100
|
+
if (info === void 0) return;
|
|
5101
|
+
if (info.node.entry.kind === "child") info.node.entry.explicitlySuspended = false;
|
|
5102
|
+
if (self.isSuspended()) return;
|
|
5103
|
+
info.instance.resume();
|
|
5104
|
+
},
|
|
5105
|
+
isItemSuspended: (key) => {
|
|
5106
|
+
const info = childMap.get(key);
|
|
5107
|
+
if (info === void 0) return false;
|
|
5108
|
+
return info.instance.isSuspended();
|
|
5109
|
+
}
|
|
3996
5110
|
};
|
|
3997
5111
|
},
|
|
3998
5112
|
lazyChild(loader, props, options) {
|
|
5113
|
+
assertLive("lazyChild");
|
|
3999
5114
|
const status$ = signal("idle");
|
|
4000
5115
|
const api$ = signal(void 0);
|
|
4001
5116
|
const error$ = signal(void 0);
|
|
4002
5117
|
let childInstance = null;
|
|
4003
|
-
let
|
|
5118
|
+
let childNode = null;
|
|
4004
5119
|
let pendingLoad = null;
|
|
4005
5120
|
let disposed = false;
|
|
4006
|
-
const
|
|
5121
|
+
const flagNode = self.entries.push({
|
|
4007
5122
|
kind: "onDispose",
|
|
4008
5123
|
fn: () => {
|
|
4009
5124
|
disposed = true;
|
|
4010
5125
|
}
|
|
4011
|
-
};
|
|
4012
|
-
self.entries.push(flagEntry);
|
|
5126
|
+
});
|
|
4013
5127
|
const handleFailure = (err) => {
|
|
4014
5128
|
status$.set("error");
|
|
4015
5129
|
error$.set(err);
|
|
@@ -4022,7 +5136,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
4022
5136
|
if (disposed) return Promise.reject(/* @__PURE__ */ new Error("[olas] ctx.lazyChild: cannot load after dispose"));
|
|
4023
5137
|
if (pendingLoad !== null) return pendingLoad;
|
|
4024
5138
|
status$.set("loading");
|
|
4025
|
-
|
|
5139
|
+
const attempt = loader().then((def) => {
|
|
4026
5140
|
if (disposed) throw new Error("[olas] ctx.lazyChild: disposed during load");
|
|
4027
5141
|
const segment = self.makeChildSegment(getFactory(def), getName(def));
|
|
4028
5142
|
const childDeps = options?.deps !== void 0 ? {
|
|
@@ -4033,11 +5147,10 @@ var ControllerInstance = class ControllerInstance {
|
|
|
4033
5147
|
try {
|
|
4034
5148
|
const api = instance.construct(getFactory(def), props);
|
|
4035
5149
|
childInstance = instance;
|
|
4036
|
-
|
|
5150
|
+
childNode = self.entries.push({
|
|
4037
5151
|
kind: "child",
|
|
4038
5152
|
instance
|
|
4039
|
-
};
|
|
4040
|
-
self.entries.push(childEntry);
|
|
5153
|
+
});
|
|
4041
5154
|
api$.set(api);
|
|
4042
5155
|
status$.set("ready");
|
|
4043
5156
|
return api;
|
|
@@ -4050,14 +5163,17 @@ var ControllerInstance = class ControllerInstance {
|
|
|
4050
5163
|
handleFailure(err);
|
|
4051
5164
|
throw err;
|
|
4052
5165
|
});
|
|
4053
|
-
|
|
5166
|
+
pendingLoad = attempt;
|
|
5167
|
+
attempt.catch(() => {
|
|
5168
|
+
if (pendingLoad === attempt) pendingLoad = null;
|
|
5169
|
+
});
|
|
5170
|
+
return attempt;
|
|
4054
5171
|
};
|
|
4055
5172
|
const dispose = () => {
|
|
4056
5173
|
if (disposed) return;
|
|
4057
5174
|
disposed = true;
|
|
4058
|
-
if (
|
|
4059
|
-
|
|
4060
|
-
if (idx >= 0) self.entries.splice(idx, 1);
|
|
5175
|
+
if (childNode !== null && childInstance !== null) {
|
|
5176
|
+
self.entries.unlink(childNode);
|
|
4061
5177
|
try {
|
|
4062
5178
|
childInstance.dispose();
|
|
4063
5179
|
} catch (err) {
|
|
@@ -4067,20 +5183,20 @@ var ControllerInstance = class ControllerInstance {
|
|
|
4067
5183
|
});
|
|
4068
5184
|
}
|
|
4069
5185
|
childInstance = null;
|
|
4070
|
-
|
|
5186
|
+
childNode = null;
|
|
4071
5187
|
}
|
|
4072
|
-
|
|
4073
|
-
if (flagIdx >= 0) self.entries.splice(flagIdx, 1);
|
|
5188
|
+
self.entries.unlink(flagNode);
|
|
4074
5189
|
};
|
|
4075
5190
|
return {
|
|
4076
|
-
status: status
|
|
4077
|
-
api: api
|
|
4078
|
-
error: error
|
|
5191
|
+
status: readOnly(status$),
|
|
5192
|
+
api: readOnly(api$),
|
|
5193
|
+
error: readOnly(error$),
|
|
4079
5194
|
load,
|
|
4080
5195
|
dispose
|
|
4081
5196
|
};
|
|
4082
5197
|
},
|
|
4083
5198
|
onDispose(fn) {
|
|
5199
|
+
assertLive("onDispose");
|
|
4084
5200
|
self.entries.push({
|
|
4085
5201
|
kind: "onDispose",
|
|
4086
5202
|
fn: () => {
|
|
@@ -4096,6 +5212,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
4096
5212
|
});
|
|
4097
5213
|
},
|
|
4098
5214
|
onSuspend(fn) {
|
|
5215
|
+
assertLive("onSuspend");
|
|
4099
5216
|
self.entries.push({
|
|
4100
5217
|
kind: "onSuspend",
|
|
4101
5218
|
fn: () => {
|
|
@@ -4111,6 +5228,7 @@ var ControllerInstance = class ControllerInstance {
|
|
|
4111
5228
|
});
|
|
4112
5229
|
},
|
|
4113
5230
|
onResume(fn) {
|
|
5231
|
+
assertLive("onResume");
|
|
4114
5232
|
self.entries.push({
|
|
4115
5233
|
kind: "onResume",
|
|
4116
5234
|
fn: () => {
|
|
@@ -4144,6 +5262,7 @@ const ROOT_METHODS = [
|
|
|
4144
5262
|
"resume",
|
|
4145
5263
|
"dehydrate",
|
|
4146
5264
|
"waitForIdle",
|
|
5265
|
+
"applyDehydratedEntry",
|
|
4147
5266
|
"__debug"
|
|
4148
5267
|
];
|
|
4149
5268
|
/**
|
|
@@ -4166,8 +5285,10 @@ function createRootWithProps(def, props, options) {
|
|
|
4166
5285
|
const instance = new ControllerInstance(null, {
|
|
4167
5286
|
devtools,
|
|
4168
5287
|
onError: options.onError,
|
|
4169
|
-
queryClient
|
|
5288
|
+
queryClient,
|
|
5289
|
+
scopesVersion: { value: 0 }
|
|
4170
5290
|
}, "root", options.deps);
|
|
5291
|
+
if (options.scopes !== void 0 && options.scopes.length > 0) instance.seedScopes(options.scopes);
|
|
4171
5292
|
let api;
|
|
4172
5293
|
try {
|
|
4173
5294
|
api = instance.construct(getFactory(def), props);
|
|
@@ -4175,8 +5296,15 @@ function createRootWithProps(def, props, options) {
|
|
|
4175
5296
|
queryClient.dispose();
|
|
4176
5297
|
throw err;
|
|
4177
5298
|
}
|
|
4178
|
-
|
|
4179
|
-
|
|
5299
|
+
let target = api;
|
|
5300
|
+
if (typeof api !== "object" || api === null) target = { value: api };
|
|
5301
|
+
try {
|
|
5302
|
+
return attachRootControls(target, instance, devtools, queryClient);
|
|
5303
|
+
} catch (err) {
|
|
5304
|
+
instance.dispose();
|
|
5305
|
+
queryClient.dispose();
|
|
5306
|
+
throw err;
|
|
5307
|
+
}
|
|
4180
5308
|
}
|
|
4181
5309
|
function attachRootControls(api, instance, devtools, queryClient) {
|
|
4182
5310
|
let suspendTimer = null;
|
|
@@ -4212,36 +5340,39 @@ function attachRootControls(api, instance, devtools, queryClient) {
|
|
|
4212
5340
|
queryEntries: () => queryClient.queryEntriesSnapshot()
|
|
4213
5341
|
};
|
|
4214
5342
|
const target = api;
|
|
4215
|
-
for (const method of ROOT_METHODS) if (
|
|
5343
|
+
for (const method of ROOT_METHODS) if (method in target) throw new Error(`[olas] Root controller api defines '${method}' which conflicts with the root controls.`);
|
|
5344
|
+
const lock = {
|
|
5345
|
+
enumerable: false,
|
|
5346
|
+
writable: false,
|
|
5347
|
+
configurable: false
|
|
5348
|
+
};
|
|
4216
5349
|
Object.defineProperty(target, "dispose", {
|
|
4217
5350
|
value: dispose,
|
|
4218
|
-
|
|
4219
|
-
configurable: true
|
|
5351
|
+
...lock
|
|
4220
5352
|
});
|
|
4221
5353
|
Object.defineProperty(target, "suspend", {
|
|
4222
5354
|
value: suspend,
|
|
4223
|
-
|
|
4224
|
-
configurable: true
|
|
5355
|
+
...lock
|
|
4225
5356
|
});
|
|
4226
5357
|
Object.defineProperty(target, "resume", {
|
|
4227
5358
|
value: resume,
|
|
4228
|
-
|
|
4229
|
-
configurable: true
|
|
5359
|
+
...lock
|
|
4230
5360
|
});
|
|
4231
5361
|
Object.defineProperty(target, "__debug", {
|
|
4232
5362
|
value: debug,
|
|
4233
|
-
|
|
4234
|
-
configurable: true
|
|
5363
|
+
...lock
|
|
4235
5364
|
});
|
|
4236
5365
|
Object.defineProperty(target, "dehydrate", {
|
|
4237
5366
|
value: () => queryClient.dehydrate(),
|
|
4238
|
-
|
|
4239
|
-
configurable: true
|
|
5367
|
+
...lock
|
|
4240
5368
|
});
|
|
4241
5369
|
Object.defineProperty(target, "waitForIdle", {
|
|
4242
5370
|
value: () => queryClient.waitForIdle(),
|
|
4243
|
-
|
|
4244
|
-
|
|
5371
|
+
...lock
|
|
5372
|
+
});
|
|
5373
|
+
Object.defineProperty(target, "applyDehydratedEntry", {
|
|
5374
|
+
value: (queryId, keyArgs, data, lastUpdatedAt) => queryClient.applyDehydratedEntry(queryId, keyArgs, data, lastUpdatedAt),
|
|
5375
|
+
...lock
|
|
4245
5376
|
});
|
|
4246
5377
|
return api;
|
|
4247
5378
|
}
|
|
@@ -4331,6 +5462,12 @@ Object.defineProperty(exports, "lookupRegisteredQuery", {
|
|
|
4331
5462
|
return lookupRegisteredQuery;
|
|
4332
5463
|
}
|
|
4333
5464
|
});
|
|
5465
|
+
Object.defineProperty(exports, "readOnly", {
|
|
5466
|
+
enumerable: true,
|
|
5467
|
+
get: function() {
|
|
5468
|
+
return readOnly;
|
|
5469
|
+
}
|
|
5470
|
+
});
|
|
4334
5471
|
Object.defineProperty(exports, "registerQueryById", {
|
|
4335
5472
|
enumerable: true,
|
|
4336
5473
|
get: function() {
|
|
@@ -4356,4 +5493,4 @@ Object.defineProperty(exports, "untracked", {
|
|
|
4356
5493
|
}
|
|
4357
5494
|
});
|
|
4358
5495
|
|
|
4359
|
-
//# sourceMappingURL=root-
|
|
5496
|
+
//# sourceMappingURL=root-CPSL5kpR.cjs.map
|