@selvajs/ui 5.0.1 → 6.0.0-beta.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/components/compute/ComputeApp.svelte +15 -7
- package/dist/components/compute/ComputeApp.svelte.d.ts +1 -1
- package/dist/components/primitives/textarea/textarea.svelte +1 -2
- package/dist/components/viewer/SceneManager.svelte +75 -160
- package/dist/components/viewer/SceneManager.svelte.d.ts +8 -3
- package/dist/components/viewer/Viewer.svelte +20 -3
- package/dist/compute/useSolveSession.svelte.d.ts +12 -0
- package/dist/compute/useSolveSession.svelte.js +76 -0
- package/dist/external/storage.d.ts +1 -15
- package/dist/external/storage.js +4 -54
- package/dist/index.d.ts +4 -3
- package/dist/index.js +7 -4
- package/dist/public.d.ts +4 -3
- package/dist/public.js +9 -5
- package/package.json +5 -3
- package/src/lib/components/compute/ComputeApp.svelte +15 -7
- package/src/lib/components/primitives/textarea/textarea.svelte +1 -2
- package/src/lib/components/viewer/SceneManager.svelte +75 -160
- package/src/lib/components/viewer/Viewer.svelte +20 -3
- package/src/lib/compute/mesh-policy-wiring.test.ts +72 -0
- package/src/lib/compute/useSolveSession.svelte.ts +83 -0
- package/src/lib/external/storage.ts +12 -64
- package/src/lib/index.ts +15 -5
- package/src/lib/public.ts +17 -6
- package/dist/compute/computeThrottle.svelte.d.ts +0 -24
- package/dist/compute/computeThrottle.svelte.js +0 -82
- package/dist/compute/createSolveSession.svelte.d.ts +0 -66
- package/dist/compute/createSolveSession.svelte.js +0 -159
- package/dist/compute/solve-session-core.d.ts +0 -54
- package/dist/compute/solve-session-core.js +0 -87
- package/dist/compute/solveMemo.d.ts +0 -22
- package/dist/compute/solveMemo.js +0 -124
- package/dist/types/solveFn.d.ts +0 -25
- package/dist/types/solveFn.js +0 -1
- package/src/lib/compute/computeThrottle.svelte.ts +0 -109
- package/src/lib/compute/computeThrottle.test.ts +0 -106
- package/src/lib/compute/createSolveSession.svelte.ts +0 -239
- package/src/lib/compute/createSolveSession.test.ts +0 -168
- package/src/lib/compute/solve-session-core.test.ts +0 -153
- package/src/lib/compute/solve-session-core.ts +0 -122
- package/src/lib/compute/solveMemo.test.ts +0 -220
- package/src/lib/compute/solveMemo.ts +0 -140
- package/src/lib/external/storage.test.ts +0 -99
- package/src/lib/types/solveFn.ts +0 -29
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The Svelte binding for a Solve Session.
|
|
2
|
+
//
|
|
3
|
+
// The session itself lives in `@selvajs/solve/client` and is framework-free: it
|
|
4
|
+
// exposes plain getters plus a `subscribe()` seam. That makes it usable headless, but a
|
|
5
|
+
// plain getter read inside Svelte markup is NOT reactive — nothing tells the component to
|
|
6
|
+
// re-run. This adapter closes that gap: it subscribes once, bumps a `$state` version
|
|
7
|
+
// counter on every notification, and reads that counter inside each getter so any
|
|
8
|
+
// component touching one re-runs when the session changes.
|
|
9
|
+
//
|
|
10
|
+
// The counter (rather than mirroring each field into its own `$state`) keeps this a pure
|
|
11
|
+
// republish: no field list to keep in sync as the session grows, and no risk of a mirrored
|
|
12
|
+
// copy drifting from the source of truth.
|
|
13
|
+
|
|
14
|
+
import type { SolveSession, SolveSessionArgs } from '@selvajs/solve/client';
|
|
15
|
+
import { createSolveSession } from '@selvajs/solve/client';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a Solve Session so its state reads reactively inside Svelte components.
|
|
19
|
+
*
|
|
20
|
+
* Returns the same `SolveSession` surface — every method delegates untouched, and every
|
|
21
|
+
* getter additionally depends on the version counter. Callers use it exactly like the
|
|
22
|
+
* session it wraps.
|
|
23
|
+
*
|
|
24
|
+
* Must be called during component initialization (it uses `$effect` to manage the
|
|
25
|
+
* subscription, so teardown follows the owning component's lifecycle).
|
|
26
|
+
*/
|
|
27
|
+
export function useSolveSession(args: SolveSessionArgs): SolveSession {
|
|
28
|
+
const session = createSolveSession(args);
|
|
29
|
+
|
|
30
|
+
// Bumped on every session notification. Reading it inside a getter is what registers
|
|
31
|
+
// the dependency; the value itself is never meaningful.
|
|
32
|
+
let version = $state(0);
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
// Re-read on mount and unsubscribe on teardown. The session outlives no component
|
|
36
|
+
// here — it is created alongside this adapter — so dropping the subscription is the
|
|
37
|
+
// whole cleanup.
|
|
38
|
+
return session.subscribe(() => {
|
|
39
|
+
version += 1;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Registers the reactive dependency, then returns the live value. */
|
|
44
|
+
function track<T>(read: () => T): T {
|
|
45
|
+
void version;
|
|
46
|
+
return read();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
get values() {
|
|
51
|
+
return track(() => session.values);
|
|
52
|
+
},
|
|
53
|
+
get error() {
|
|
54
|
+
return track(() => session.error);
|
|
55
|
+
},
|
|
56
|
+
get computeErrors() {
|
|
57
|
+
return track(() => session.computeErrors);
|
|
58
|
+
},
|
|
59
|
+
get computeWarnings() {
|
|
60
|
+
return track(() => session.computeWarnings);
|
|
61
|
+
},
|
|
62
|
+
get meshes() {
|
|
63
|
+
return track(() => session.meshes);
|
|
64
|
+
},
|
|
65
|
+
get hasPendingChanges() {
|
|
66
|
+
return track(() => session.hasPendingChanges);
|
|
67
|
+
},
|
|
68
|
+
get hasNeverSolved() {
|
|
69
|
+
return track(() => session.hasNeverSolved);
|
|
70
|
+
},
|
|
71
|
+
get isSolving() {
|
|
72
|
+
return track(() => session.isSolving);
|
|
73
|
+
},
|
|
74
|
+
setValue: (id, value, forceSolve) => session.setValue(id, value, forceSolve),
|
|
75
|
+
solve: () => session.solve(),
|
|
76
|
+
loadValues: (incoming) => session.loadValues(incoming),
|
|
77
|
+
rebuild: (schema, scopeKey) => session.rebuild(schema, scopeKey),
|
|
78
|
+
report: (result) => session.report(result),
|
|
79
|
+
reportError: (message) => session.reportError(message),
|
|
80
|
+
subscribe: (listener) => session.subscribe(listener),
|
|
81
|
+
notify: () => session.notify()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -1,64 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const STORAGE_PREFIX = 'external';
|
|
15
|
-
|
|
16
|
-
function makeKey(scopeKey: string, inputId: string): string {
|
|
17
|
-
return `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ExternalValueRef {
|
|
21
|
-
scopeKey: string;
|
|
22
|
-
inputId: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {
|
|
26
|
-
const { scopeKey, inputId, value } = args;
|
|
27
|
-
if (!scopeKey || !inputId) return;
|
|
28
|
-
if (typeof sessionStorage === 'undefined') return;
|
|
29
|
-
sessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function readExternalValue(ref: ExternalValueRef): unknown | undefined {
|
|
33
|
-
const { scopeKey, inputId } = ref;
|
|
34
|
-
if (!scopeKey || !inputId) return undefined;
|
|
35
|
-
if (typeof sessionStorage === 'undefined') return undefined;
|
|
36
|
-
const raw = sessionStorage.getItem(makeKey(scopeKey, inputId));
|
|
37
|
-
if (raw === null) return undefined;
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(raw);
|
|
40
|
-
} catch {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function clearExternalValue(ref: ExternalValueRef): void {
|
|
46
|
-
const { scopeKey, inputId } = ref;
|
|
47
|
-
if (!scopeKey || !inputId) return;
|
|
48
|
-
if (typeof sessionStorage === 'undefined') return;
|
|
49
|
-
sessionStorage.removeItem(makeKey(scopeKey, inputId));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ExternalInput {
|
|
53
|
-
paramId: string;
|
|
54
|
-
displayName: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getExternalInputs(schema: UISchema): ExternalInput[] {
|
|
58
|
-
return getInputItems(schema)
|
|
59
|
-
.filter((item) => item.source?.kind === 'client')
|
|
60
|
-
.map((item) => ({
|
|
61
|
-
paramId: item.paramId,
|
|
62
|
-
displayName: item.displayName ?? item.paramId
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
1
|
+
// Moved to `@selvajs/solve/client`, where the Solve Session that hydrates from it
|
|
2
|
+
// now lives. Re-exported here to keep the published `@selvajs/ui/external` sub-path — and
|
|
3
|
+
// the pre-step producer routes that import it — working unchanged.
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
writeExternalValue,
|
|
7
|
+
readExternalValue,
|
|
8
|
+
clearExternalValue,
|
|
9
|
+
getExternalInputs,
|
|
10
|
+
type ExternalValueRef,
|
|
11
|
+
type ExternalInput
|
|
12
|
+
} from '@selvajs/solve/client';
|
package/src/lib/index.ts
CHANGED
|
@@ -21,9 +21,12 @@ export * from './schema/dynamic-value-list';
|
|
|
21
21
|
export * from './schema/traversal';
|
|
22
22
|
export * from './compute/solving.svelte';
|
|
23
23
|
|
|
24
|
-
// Solve Session seam
|
|
25
|
-
//
|
|
24
|
+
// Solve Session seam. The session itself now lives in `@selvajs/solve/client`
|
|
25
|
+
// (framework-free); `useSolveSession` is this package's Svelte binding, which republishes
|
|
26
|
+
// the session's subscribe() notifications as rune state so its getters read reactively in
|
|
27
|
+
// markup. Re-exported so transports outside this package — e.g. plugin-ui's WebSocket
|
|
26
28
|
// driver — can satisfy SolveDriver and drive a session. See CONTEXT.md.
|
|
29
|
+
export { useSolveSession } from './compute/useSolveSession.svelte';
|
|
27
30
|
export {
|
|
28
31
|
createSolveSession,
|
|
29
32
|
createRequestResponseDriver,
|
|
@@ -31,10 +34,17 @@ export {
|
|
|
31
34
|
type SolveSessionArgs,
|
|
32
35
|
type SolveDriver,
|
|
33
36
|
type SolveReporter
|
|
34
|
-
} from '
|
|
37
|
+
} from '@selvajs/solve/client';
|
|
35
38
|
|
|
36
39
|
// External-input transit storage (used by routes that wire pre-step producers)
|
|
37
|
-
export
|
|
40
|
+
export {
|
|
41
|
+
writeExternalValue,
|
|
42
|
+
readExternalValue,
|
|
43
|
+
clearExternalValue,
|
|
44
|
+
getExternalInputs,
|
|
45
|
+
type ExternalValueRef,
|
|
46
|
+
type ExternalInput
|
|
47
|
+
} from '@selvajs/solve/client';
|
|
38
48
|
|
|
39
49
|
// Contexts & Composables
|
|
40
50
|
export * from './contexts/footerContext.svelte';
|
|
@@ -47,5 +57,5 @@ export { randomId } from './utils/randomId';
|
|
|
47
57
|
|
|
48
58
|
// UI-specific runtime types (not from schema)
|
|
49
59
|
export type { ActionButton } from './types/actionButton';
|
|
50
|
-
export type { SolveFn, SolveResult } from '
|
|
60
|
+
export type { SolveFn, SolveResult } from '@selvajs/solve/shared';
|
|
51
61
|
export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
|
package/src/lib/public.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// Scope: the compute-app SDK — everything an external host app needs to embed a
|
|
9
9
|
// Grasshopper-driven app (ComputeApp), drive solves, and wire pre-step
|
|
10
10
|
// producers. Verified against real external host apps: they import
|
|
11
|
-
// ComputeApp + its types, the solve seam, and external
|
|
11
|
+
// ComputeApp + its types, the solve seam, and the external-input storage
|
|
12
|
+
// helpers. Nothing else.
|
|
12
13
|
//
|
|
13
14
|
// Deliberately NOT public: design-system primitives (Button, Card, Dialog, …),
|
|
14
15
|
// page-chrome layout (AppShell, SideNav, …), toast/Toaster, ThemeSwitcher,
|
|
@@ -44,8 +45,11 @@ export {
|
|
|
44
45
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
45
46
|
|
|
46
47
|
// Solve Session seam (transport-agnostic value/lifecycle state machine + its
|
|
47
|
-
// driver interface).
|
|
48
|
-
//
|
|
48
|
+
// driver interface). The session lives in `@selvajs/solve/client` and is
|
|
49
|
+
// framework-free; `useSolveSession` is the Svelte binding that makes its getters
|
|
50
|
+
// read reactively inside components. A host embedding <ComputeApp> needs neither —
|
|
51
|
+
// both are re-exported for hosts driving a session themselves. See CONTEXT.md.
|
|
52
|
+
export { useSolveSession } from './compute/useSolveSession.svelte';
|
|
49
53
|
export {
|
|
50
54
|
createSolveSession,
|
|
51
55
|
createRequestResponseDriver,
|
|
@@ -53,14 +57,21 @@ export {
|
|
|
53
57
|
type SolveSessionArgs,
|
|
54
58
|
type SolveDriver,
|
|
55
59
|
type SolveReporter
|
|
56
|
-
} from '
|
|
60
|
+
} from '@selvajs/solve/client';
|
|
57
61
|
|
|
58
62
|
// Client-slot context type (host apps render their own cell for client-sourced
|
|
59
63
|
// inputs, and may commit a value back via ClientSlotArgs.onValueChange).
|
|
60
64
|
export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
|
|
61
65
|
|
|
62
66
|
// Pre-step producer transit storage (host apps wire producers via these).
|
|
63
|
-
export
|
|
67
|
+
export {
|
|
68
|
+
writeExternalValue,
|
|
69
|
+
readExternalValue,
|
|
70
|
+
clearExternalValue,
|
|
71
|
+
getExternalInputs,
|
|
72
|
+
type ExternalValueRef,
|
|
73
|
+
type ExternalInput
|
|
74
|
+
} from '@selvajs/solve/client';
|
|
64
75
|
|
|
65
76
|
// Schema utilities a ComputeApp host reasonably needs to read/shape values.
|
|
66
77
|
export * from './schema/defaults';
|
|
@@ -69,5 +80,5 @@ export * from './schema/dynamic-value-list';
|
|
|
69
80
|
|
|
70
81
|
// UI-facing runtime types (not from schema)
|
|
71
82
|
export type { ActionButton } from './types/actionButton';
|
|
72
|
-
export type { SolveFn, SolveResult } from '
|
|
83
|
+
export type { SolveFn, SolveResult } from '@selvajs/solve/shared';
|
|
73
84
|
export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compute throttle utility for managing async compute requests.
|
|
3
|
-
*
|
|
4
|
-
* - Only one request in-flight at a time
|
|
5
|
-
* - Latest values always win (no queue)
|
|
6
|
-
* - AbortController support to cancel stale requests
|
|
7
|
-
* - Configurable timeout with automatic abort
|
|
8
|
-
*/
|
|
9
|
-
interface ComputeThrottleOptions {
|
|
10
|
-
timeout?: number;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Creates a throttled compute handler that keeps only one request in-flight.
|
|
14
|
-
* If a new value arrives while one is running, it overwrites the single pending
|
|
15
|
-
* slot — older pending values are dropped. The pending value runs once the
|
|
16
|
-
* current request finishes (or aborts).
|
|
17
|
-
*/
|
|
18
|
-
export declare function createComputeThrottle<T>(computeFn: (values: T, signal: AbortSignal) => Promise<void>, options?: ComputeThrottleOptions): {
|
|
19
|
-
trigger: (values: T) => void;
|
|
20
|
-
readonly isComputing: boolean;
|
|
21
|
-
readonly hasPending: boolean;
|
|
22
|
-
cancel: () => void;
|
|
23
|
-
};
|
|
24
|
-
export {};
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fallback per-solve abort timeout (ms) when the caller doesn't pass one.
|
|
3
|
-
* Used only by callers without a deployment-specific limit (e.g. plugin-ui
|
|
4
|
-
* over WebSocket); the selva app supplies `MAX_SOLVE_DURATION_MS` from its
|
|
5
|
-
* server config via `ComputeApp`'s `solveTimeoutMs` prop.
|
|
6
|
-
*/
|
|
7
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
8
|
-
/**
|
|
9
|
-
* Creates a throttled compute handler that keeps only one request in-flight.
|
|
10
|
-
* If a new value arrives while one is running, it overwrites the single pending
|
|
11
|
-
* slot — older pending values are dropped. The pending value runs once the
|
|
12
|
-
* current request finishes (or aborts).
|
|
13
|
-
*/
|
|
14
|
-
export function createComputeThrottle(computeFn, options = {}) {
|
|
15
|
-
const { timeout = DEFAULT_TIMEOUT_MS } = options;
|
|
16
|
-
let isComputing = $state(false);
|
|
17
|
-
let pendingValues = $state(null);
|
|
18
|
-
let currentAbortController = null;
|
|
19
|
-
function abortCurrent() {
|
|
20
|
-
currentAbortController?.abort();
|
|
21
|
-
currentAbortController = null;
|
|
22
|
-
}
|
|
23
|
-
async function executeCompute(values) {
|
|
24
|
-
abortCurrent();
|
|
25
|
-
currentAbortController = new AbortController();
|
|
26
|
-
const { signal } = currentAbortController;
|
|
27
|
-
const timeoutId = setTimeout(() => {
|
|
28
|
-
// Cleared in `finally` on every other path, so firing means a genuine
|
|
29
|
-
// timeout — the only signal for it (the abort itself is swallowed below).
|
|
30
|
-
console.warn(`[Compute/throttle] solve exceeded ${timeout}ms — aborting`);
|
|
31
|
-
currentAbortController?.abort();
|
|
32
|
-
}, timeout);
|
|
33
|
-
isComputing = true;
|
|
34
|
-
try {
|
|
35
|
-
await computeFn(values, signal);
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
// AbortError is expected (timeout or cancel). Non-abort errors must be
|
|
39
|
-
// handled inside computeFn — re-throwing here would produce an unhandled
|
|
40
|
-
// rejection because executeCompute is always called fire-and-forget.
|
|
41
|
-
if (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {
|
|
42
|
-
console.error('[computeThrottle] unhandled error in computeFn:', err);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
clearTimeout(timeoutId);
|
|
47
|
-
isComputing = false;
|
|
48
|
-
currentAbortController = null;
|
|
49
|
-
if (pendingValues !== null) {
|
|
50
|
-
const next = pendingValues;
|
|
51
|
-
pendingValues = null;
|
|
52
|
-
executeCompute(next);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function trigger(values) {
|
|
57
|
-
if (isComputing) {
|
|
58
|
-
if (pendingValues !== null) {
|
|
59
|
-
// Latest-wins: the previously-queued values are dropped, not solved.
|
|
60
|
-
console.debug('[Compute/throttle] superseded pending solve (latest-wins)');
|
|
61
|
-
}
|
|
62
|
-
pendingValues = values;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
executeCompute(values);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function cancel() {
|
|
69
|
-
pendingValues = null;
|
|
70
|
-
abortCurrent();
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
trigger,
|
|
74
|
-
get isComputing() {
|
|
75
|
-
return isComputing;
|
|
76
|
-
},
|
|
77
|
-
get hasPending() {
|
|
78
|
-
return pendingValues !== null;
|
|
79
|
-
},
|
|
80
|
-
cancel
|
|
81
|
-
};
|
|
82
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { UISchema } from '@selvajs/schemas';
|
|
2
|
-
import type { SolveFn, SolveResult } from '../types/solveFn';
|
|
3
|
-
/**
|
|
4
|
-
* The transport behind a Solve Session. Knows how to start and cancel a solve and
|
|
5
|
-
* reports its in-flight state. It does NOT return outputs — those come back via the
|
|
6
|
-
* session's report() so push transports (WebSocket) fit without contortion.
|
|
7
|
-
*/
|
|
8
|
-
export interface SolveDriver {
|
|
9
|
-
solve(values: Record<string, unknown>): void;
|
|
10
|
-
cancel(): void;
|
|
11
|
-
readonly isSolving: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Drops any cached solve results the driver holds. Optional — only drivers with a
|
|
14
|
-
* client-side memo (the request/response driver) implement it. Called on rebuild so a
|
|
15
|
-
* definition swap can't serve a stale result from a prior definition's input space.
|
|
16
|
-
*/
|
|
17
|
-
clearCache?(): void;
|
|
18
|
-
}
|
|
19
|
-
export interface SolveSession {
|
|
20
|
-
readonly values: Record<string, unknown>;
|
|
21
|
-
readonly error: string;
|
|
22
|
-
readonly computeErrors: string[];
|
|
23
|
-
readonly computeWarnings: string[];
|
|
24
|
-
readonly meshes: unknown[];
|
|
25
|
-
readonly hasPendingChanges: boolean;
|
|
26
|
-
readonly hasNeverSolved: boolean;
|
|
27
|
-
readonly isSolving: boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Records a value change and dispatches a solve unless the schema is manual-solve.
|
|
30
|
-
* `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic
|
|
31
|
-
* value list pruning a vanished selection), so the output can't lag behind the new value.
|
|
32
|
-
*/
|
|
33
|
-
setValue(id: string, value: unknown, forceSolve?: boolean): void;
|
|
34
|
-
/** Explicit "calculate" — dispatches a solve with the current values. */
|
|
35
|
-
solve(): void;
|
|
36
|
-
/** Merges incoming values, then solves (auto) or marks dirty (manual). */
|
|
37
|
-
loadValues(incoming: Record<string, unknown>): void;
|
|
38
|
-
/** Re-seed for a new active definition: rebuild values, clear outputs, gate the solve. */
|
|
39
|
-
rebuild(schema: UISchema, scopeKey: string): void;
|
|
40
|
-
/** Feed a completed solve result back into the session (called by the driver/host). */
|
|
41
|
-
report(result: SolveResult): void;
|
|
42
|
-
/** Report a solve failure (transport/solver error) — surfaces in `error`. */
|
|
43
|
-
reportError(message: string): void;
|
|
44
|
-
}
|
|
45
|
-
export interface SolveSessionArgs {
|
|
46
|
-
schema: UISchema;
|
|
47
|
-
scopeKey: string;
|
|
48
|
-
driver: SolveDriver;
|
|
49
|
-
}
|
|
50
|
-
export declare function createSolveSession(args: SolveSessionArgs): SolveSession;
|
|
51
|
-
/** The slice of a SolveSession a driver feeds completed/failed solves back into. */
|
|
52
|
-
export interface SolveReporter {
|
|
53
|
-
report(result: SolveResult): void;
|
|
54
|
-
reportError(message: string): void;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Request/response Solve Driver: wraps createComputeThrottle around a SolveFn and feeds
|
|
58
|
-
* the resolved result back through the reporter. One solve in flight at a time; the
|
|
59
|
-
* latest triggered values win. Used by ComputeApp (Rhino.Compute over HTTP).
|
|
60
|
-
*
|
|
61
|
-
* Because the session and driver reference each other, the host passes the reporter
|
|
62
|
-
* lazily (`() => session`) so it can construct the session with the driver in hand.
|
|
63
|
-
*/
|
|
64
|
-
export declare function createRequestResponseDriver(onSolve: SolveFn, getReporter: () => SolveReporter, options?: {
|
|
65
|
-
timeout?: number;
|
|
66
|
-
}): SolveDriver;
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
// Reactive Solve Session (see CONTEXT.md): the $state-backed shell over the pure
|
|
2
|
-
// transition logic in solve-session-core.ts. It owns the live values/flags, delegates
|
|
3
|
-
// every transition to the core, and drives solves through a transport-agnostic
|
|
4
|
-
// SolveDriver. A completed solve re-enters via report().
|
|
5
|
-
import { readExternalValue } from '../external/storage';
|
|
6
|
-
import { createComputeThrottle } from './computeThrottle.svelte';
|
|
7
|
-
import { createSolveMemo } from './solveMemo';
|
|
8
|
-
import { buildInitialValues, makeInitialFlags, applyValueChange, applySolveResult, pickInputValues } from './solve-session-core';
|
|
9
|
-
export function createSolveSession(args) {
|
|
10
|
-
let currentSchema = args.schema;
|
|
11
|
-
const flags = makeInitialFlags(currentSchema?.instanceSolve);
|
|
12
|
-
const state = $state({
|
|
13
|
-
values: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),
|
|
14
|
-
error: '',
|
|
15
|
-
computeErrors: [],
|
|
16
|
-
computeWarnings: [],
|
|
17
|
-
meshes: [],
|
|
18
|
-
pendingValues: {},
|
|
19
|
-
hasPendingChanges: flags.hasPendingChanges,
|
|
20
|
-
hasNeverSolved: flags.hasNeverSolved
|
|
21
|
-
});
|
|
22
|
-
function dispatch() {
|
|
23
|
-
// Input values only: outputs merged into state.values (for widgets that read
|
|
24
|
-
// them, e.g. dynamic value lists) must not be echoed back to the transport.
|
|
25
|
-
args.driver.solve(pickInputValues(currentSchema, $state.snapshot(state.values)));
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
get values() {
|
|
29
|
-
return state.values;
|
|
30
|
-
},
|
|
31
|
-
get error() {
|
|
32
|
-
return state.error;
|
|
33
|
-
},
|
|
34
|
-
get computeErrors() {
|
|
35
|
-
return state.computeErrors;
|
|
36
|
-
},
|
|
37
|
-
get computeWarnings() {
|
|
38
|
-
return state.computeWarnings;
|
|
39
|
-
},
|
|
40
|
-
get meshes() {
|
|
41
|
-
return state.meshes;
|
|
42
|
-
},
|
|
43
|
-
get hasPendingChanges() {
|
|
44
|
-
return state.hasPendingChanges;
|
|
45
|
-
},
|
|
46
|
-
get hasNeverSolved() {
|
|
47
|
-
return state.hasNeverSolved;
|
|
48
|
-
},
|
|
49
|
-
get isSolving() {
|
|
50
|
-
return args.driver.isSolving;
|
|
51
|
-
},
|
|
52
|
-
setValue(id, value, forceSolve = false) {
|
|
53
|
-
const { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);
|
|
54
|
-
if (shouldSolve || forceSolve) {
|
|
55
|
-
// A forced solve reconciles the deferred output; clear the dirty flags it raised.
|
|
56
|
-
if (forceSolve && !shouldSolve) {
|
|
57
|
-
state.pendingValues = {};
|
|
58
|
-
state.hasPendingChanges = false;
|
|
59
|
-
}
|
|
60
|
-
dispatch();
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
solve() {
|
|
64
|
-
dispatch();
|
|
65
|
-
},
|
|
66
|
-
loadValues(incoming) {
|
|
67
|
-
Object.assign(state.values, incoming);
|
|
68
|
-
if (currentSchema?.instanceSolve !== false) {
|
|
69
|
-
dispatch();
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
state.hasPendingChanges = true;
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
rebuild(schema, scopeKey) {
|
|
76
|
-
currentSchema = schema;
|
|
77
|
-
// Drop the driver's result memo: the new definition has its own input space, so
|
|
78
|
-
// a matching input key from the prior definition must not serve its stale result.
|
|
79
|
-
args.driver.clearCache?.();
|
|
80
|
-
state.meshes = [];
|
|
81
|
-
state.error = '';
|
|
82
|
-
state.computeErrors = [];
|
|
83
|
-
state.computeWarnings = [];
|
|
84
|
-
state.pendingValues = {};
|
|
85
|
-
state.values = buildInitialValues(schema, scopeKey, readExternalValue);
|
|
86
|
-
const f = makeInitialFlags(schema?.instanceSolve);
|
|
87
|
-
state.hasPendingChanges = f.hasPendingChanges;
|
|
88
|
-
state.hasNeverSolved = f.hasNeverSolved;
|
|
89
|
-
if (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {
|
|
90
|
-
dispatch();
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
report(result) {
|
|
94
|
-
applySolveResult(state, result);
|
|
95
|
-
},
|
|
96
|
-
reportError(message) {
|
|
97
|
-
state.error = message;
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Request/response Solve Driver: wraps createComputeThrottle around a SolveFn and feeds
|
|
103
|
-
* the resolved result back through the reporter. One solve in flight at a time; the
|
|
104
|
-
* latest triggered values win. Used by ComputeApp (Rhino.Compute over HTTP).
|
|
105
|
-
*
|
|
106
|
-
* Because the session and driver reference each other, the host passes the reporter
|
|
107
|
-
* lazily (`() => session`) so it can construct the session with the driver in hand.
|
|
108
|
-
*/
|
|
109
|
-
export function createRequestResponseDriver(onSolve, getReporter, options = {}) {
|
|
110
|
-
// M2: a small LRU memoizing completed solves by their input values. A slider dragged
|
|
111
|
-
// back to a value already solved this session reports instantly without a network
|
|
112
|
-
// round-trip. The check lives inside the throttled computeFn so the throttle's
|
|
113
|
-
// latest-wins ordering still holds — a hit only serves after the throttle picks these
|
|
114
|
-
// values as the ones to run.
|
|
115
|
-
const memo = createSolveMemo();
|
|
116
|
-
const throttle = createComputeThrottle(async (values, signal) => {
|
|
117
|
-
const cached = memo.get(values);
|
|
118
|
-
if (cached !== undefined) {
|
|
119
|
-
if (signal.aborted)
|
|
120
|
-
return;
|
|
121
|
-
getReporter().report(cached);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
const result = await onSolve(values, signal);
|
|
126
|
-
if (signal.aborted) {
|
|
127
|
-
// Discarded on purpose (superseded/cancelled) — never memoized or reported.
|
|
128
|
-
console.debug('[Compute/session] solve completed after abort — result discarded');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
memo.set(values, result);
|
|
132
|
-
getReporter().report(result);
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
if (signal.aborted) {
|
|
136
|
-
console.debug('[Compute/session] solve aborted (superseded, cancelled, or timed out)');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
// reportError only sets reactive state; without this line a transport
|
|
140
|
-
// failure leaves no console trace at all.
|
|
141
|
-
console.warn('[Compute/session] solve failed:', err);
|
|
142
|
-
getReporter().reportError(err instanceof Error ? err.message : String(err));
|
|
143
|
-
}
|
|
144
|
-
}, options);
|
|
145
|
-
return {
|
|
146
|
-
solve(values) {
|
|
147
|
-
throttle.trigger(values);
|
|
148
|
-
},
|
|
149
|
-
cancel() {
|
|
150
|
-
throttle.cancel();
|
|
151
|
-
},
|
|
152
|
-
get isSolving() {
|
|
153
|
-
return throttle.isComputing;
|
|
154
|
-
},
|
|
155
|
-
clearCache() {
|
|
156
|
-
memo.clear();
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { UISchema } from '@selvajs/schemas';
|
|
2
|
-
import { type ExternalValueRef } from '../external/storage';
|
|
3
|
-
import type { SolveResult } from '../types/solveFn';
|
|
4
|
-
export interface SolveSessionState {
|
|
5
|
-
values: Record<string, unknown>;
|
|
6
|
-
error: string;
|
|
7
|
-
computeErrors: string[];
|
|
8
|
-
computeWarnings: string[];
|
|
9
|
-
meshes: unknown[];
|
|
10
|
-
/** Values changed since the last solve, in manual (instanceSolve === false) mode. */
|
|
11
|
-
pendingValues: Record<string, unknown>;
|
|
12
|
-
hasPendingChanges: boolean;
|
|
13
|
-
hasNeverSolved: boolean;
|
|
14
|
-
}
|
|
15
|
-
/** Reads a previously produced client-sourced value, or undefined if absent. */
|
|
16
|
-
export type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;
|
|
17
|
-
/**
|
|
18
|
-
* Builds the initial `values` map for a schema. Non-client inputs get their declared
|
|
19
|
-
* default (falling back to the paramType default); client-sourced inputs are hydrated
|
|
20
|
-
* from `read` and left absent when no stored value exists so the missing-inputs panel
|
|
21
|
-
* can detect them. Outputs are seeded to null.
|
|
22
|
-
*/
|
|
23
|
-
export declare function buildInitialValues(schema: UISchema, scopeKey: string, read: ExternalReader): Record<string, unknown>;
|
|
24
|
-
/**
|
|
25
|
-
* Initial lifecycle flags. Manual-solve schemas (instanceSolve === false) start dirty
|
|
26
|
-
* so the user must explicitly calculate; auto-solve schemas start clean.
|
|
27
|
-
*/
|
|
28
|
-
export declare function makeInitialFlags(instanceSolve: boolean | undefined): {
|
|
29
|
-
hasPendingChanges: boolean;
|
|
30
|
-
hasNeverSolved: boolean;
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Records a single value change and decides whether it should dispatch a solve now.
|
|
34
|
-
* Auto-solve mode dispatches immediately; manual mode defers (records the pending value
|
|
35
|
-
* and raises the dirty flag) and never dispatches.
|
|
36
|
-
*/
|
|
37
|
-
export declare function applyValueChange(state: SolveSessionState, id: string, value: unknown, instanceSolve: boolean | undefined): {
|
|
38
|
-
state: SolveSessionState;
|
|
39
|
-
shouldSolve: boolean;
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* Projects the session's live values down to solve INPUTS. Solve outputs are merged
|
|
43
|
-
* into the same values map after each solve (applySolveResult) so widgets like
|
|
44
|
-
* dynamic value lists can read them — but they are not solve inputs, and echoing
|
|
45
|
-
* them back to the driver re-uploads potentially MB-sized payloads (a measured
|
|
46
|
-
* 6.4 MB options list) that no backend reads. Every transport gets this projection
|
|
47
|
-
* for free by going through the session's dispatch.
|
|
48
|
-
*/
|
|
49
|
-
export declare function pickInputValues(schema: UISchema | undefined, values: Record<string, unknown>): Record<string, unknown>;
|
|
50
|
-
/**
|
|
51
|
-
* Merges a reported solve result into the state and clears the post-solve lifecycle
|
|
52
|
-
* flags. Missing result arrays are treated as empty.
|
|
53
|
-
*/
|
|
54
|
-
export declare function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState;
|