@fjader/svelte 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +1 -1
  2. package/dist/animation/EntranceGate.d.ts +52 -0
  3. package/dist/animation/EntranceGate.js +93 -0
  4. package/dist/animation/TransitionClock.d.ts +61 -30
  5. package/dist/animation/TransitionClock.js +67 -29
  6. package/dist/animation/transition-log.d.ts +41 -0
  7. package/dist/animation/transition-log.js +57 -0
  8. package/dist/components/axes/AxisBottom.svelte +4 -2
  9. package/dist/components/axes/AxisCenter.svelte +4 -2
  10. package/dist/components/axes/AxisLeft.svelte +4 -2
  11. package/dist/components/axes/BandAxisTicks.svelte +3 -2
  12. package/dist/components/charts/ChartCell.svelte +32 -5
  13. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  14. package/dist/components/charts/ChartContainer.svelte +45 -3
  15. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -1
  16. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  17. package/dist/components/charts/ChartHover.svelte.js +22 -0
  18. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  19. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  20. package/dist/components/guides/Grid.svelte +12 -1
  21. package/dist/components/marks/AreaMarkX.svelte +16 -4
  22. package/dist/components/marks/AreaMarkY.svelte +16 -4
  23. package/dist/components/marks/BarMarkX.svelte +7 -3
  24. package/dist/components/marks/BarMarkY.svelte +7 -3
  25. package/dist/components/marks/DotsMark.svelte +6 -3
  26. package/dist/components/marks/LineMark.svelte +11 -8
  27. package/dist/components/marks/ParallelCoordinatesMark.svelte +7 -2
  28. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
  29. package/dist/components/marks/RectMark.svelte +6 -3
  30. package/dist/components/marks/RectXMark.svelte +7 -3
  31. package/dist/components/marks/RectYMark.svelte +7 -3
  32. package/dist/components/marks/RuleMarkX.svelte +30 -26
  33. package/dist/components/marks/RuleMarkY.svelte +30 -26
  34. package/dist/components/marks/TextMark.svelte +44 -5
  35. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +16 -0
  36. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +398 -54
  37. package/dist/components/marks/lifecycle/domain-queries.d.ts +11 -2
  38. package/dist/components/marks/lifecycle/domain-queries.js +33 -6
  39. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  40. package/dist/components/marks/lifecycle/domain-registration.js +15 -14
  41. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +9 -1
  42. package/dist/components/marks/lifecycle/other-rows.svelte.js +2 -2
  43. package/dist/components/marks/lifecycle/query-builder.d.ts +27 -2
  44. package/dist/components/marks/lifecycle/query-builder.js +23 -20
  45. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  46. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  47. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  48. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  49. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  50. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  51. package/dist/components/marks/utils/reactive-config.js +3 -0
  52. package/dist/components/rendering/Areas.svelte +57 -19
  53. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  54. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +1 -1
  55. package/dist/components/rendering/Dots.svelte +23 -3
  56. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  57. package/dist/components/rendering/GridLine.svelte +13 -1
  58. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  59. package/dist/components/rendering/Lines.svelte +48 -13
  60. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  61. package/dist/components/rendering/ParallelLines.svelte +5 -5
  62. package/dist/components/rendering/Rects.svelte +51 -11
  63. package/dist/components/rendering/Rects.svelte.d.ts +16 -2
  64. package/dist/components/rendering/RuleX.svelte +1 -1
  65. package/dist/components/rendering/RuleY.svelte +1 -1
  66. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  67. package/dist/components/rendering/activity-reads.d.ts +41 -24
  68. package/dist/components/rendering/activity-reads.js +54 -31
  69. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  70. package/dist/components/rendering/uniform-sizes.js +1 -1
  71. package/dist/config/ChartConfig.svelte.d.ts +7 -0
  72. package/dist/config/ChartConfig.svelte.js +17 -0
  73. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  74. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  75. package/dist/gpu/MorphedChannels.d.ts +11 -2
  76. package/dist/gpu/MorphedChannels.js +33 -11
  77. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  78. package/dist/gpu/ScaleTemplateSystem.js +58 -2
  79. package/dist/index.d.ts +17 -4
  80. package/dist/index.js +19 -4
  81. package/dist/interactors/TooltipManager.svelte.js +14 -5
  82. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  83. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  84. package/dist/query-engine/FieldActivity.svelte.d.ts +2 -3
  85. package/dist/query-engine/FieldActivity.svelte.js +1 -2
  86. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  87. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  88. package/dist/query-engine/QueryEngine.d.ts +225 -5
  89. package/dist/query-engine/QueryEngine.js +671 -41
  90. package/dist/query-engine/categorical-identity.d.ts +34 -8
  91. package/dist/query-engine/categorical-identity.js +138 -73
  92. package/dist/query-engine/debug/query-log.d.ts +39 -0
  93. package/dist/query-engine/debug/query-log.js +90 -0
  94. package/dist/query-engine/duckdb.d.ts +55 -8
  95. package/dist/query-engine/duckdb.js +82 -30
  96. package/dist/query-engine/index.d.ts +7 -4
  97. package/dist/query-engine/index.js +3 -3
  98. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  99. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  100. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  101. package/dist/query-engine/preagg/Preagg.js +159 -31
  102. package/dist/query-engine/preagg/plan.d.ts +28 -1
  103. package/dist/query-engine/preagg/plan.js +128 -8
  104. package/dist/query-engine/sliced-lane.d.ts +108 -0
  105. package/dist/query-engine/sliced-lane.js +299 -0
  106. package/dist/query-engine/source-routing.d.ts +106 -0
  107. package/dist/query-engine/source-routing.js +189 -0
  108. package/dist/scales/BandScaleState.svelte.d.ts +9 -1
  109. package/dist/scales/BandScaleState.svelte.js +16 -5
  110. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  111. package/dist/scales/ChartScales.svelte.js +26 -8
  112. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  113. package/dist/scales/ColorScaleState.svelte.js +138 -26
  114. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  115. package/dist/scales/LinearScaleState.svelte.js +21 -39
  116. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  117. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  118. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  119. package/dist/scales/SlotTransition.d.ts +15 -1
  120. package/dist/scales/SlotTransition.js +33 -4
  121. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  122. package/dist/scales/TimeScaleState.svelte.js +7 -7
  123. package/dist/scales/color-utils.d.ts +0 -3
  124. package/dist/scales/color-utils.js +0 -3
  125. package/dist/scales/entered.d.ts +8 -0
  126. package/dist/scales/entered.js +10 -0
  127. package/dist/spec/LazyQueryEngine.js +2 -19
  128. package/dist/spec/MarkRegistry.d.ts +3 -1
  129. package/dist/spec/MarkRegistry.js +13 -1
  130. package/dist/spec/PlotFromSpec.svelte +1 -0
  131. package/dist/spec/export.js +11 -2
  132. package/dist/spec/types.d.ts +41 -13
  133. package/dist/sql/bin.d.ts +25 -0
  134. package/dist/sql/bin.js +108 -5
  135. package/dist/transforms/TransformCompiler.d.ts +10 -0
  136. package/dist/transforms/TransformCompiler.js +26 -0
  137. package/dist/transforms/index.js +144 -24
  138. package/dist/types.d.ts +136 -2
  139. package/package.json +2 -2
@@ -0,0 +1,299 @@
1
+ /**
2
+ * A sliced lane: one DuckDB-WASM connection whose statements run through the
3
+ * pending-query API, so the worker executes them in slices and serves other
4
+ * connections' messages in between. A slow read on a lane no longer holds up a
5
+ * quick one on another connection.
6
+ *
7
+ * ONE STATEMENT AT A TIME, enforced here. A connection holds one pending query,
8
+ * and a second one, or a `runQuery`, on the same connection silently resets the
9
+ * first (measured in Phase 0). So the lane queues: first in, first out.
10
+ *
11
+ * ABORT (an AbortSignal per statement) has three cases:
12
+ * - queued: dropped, rejected with AbortError, never sent;
13
+ * - running, before the result header: `cancelPendingQuery`, rejected with
14
+ * AbortError. Only an abort we asked for becomes AbortError; any other error
15
+ * passes through unchanged;
16
+ * - after the header, fetching chunks: the query has finished executing, so
17
+ * there is nothing to cancel. Fetching stops and the statement rejects; the
18
+ * next `startPendingQuery` resets the leftover result.
19
+ * A cancel that loses the race (the query finished as it arrived, so the next
20
+ * poll returns the header) still rejects with AbortError: the caller sees one
21
+ * outcome for one abort.
22
+ *
23
+ * SLICE LENGTH is the database's `queryPollingInterval`, set when it is opened
24
+ * (`initDuckDB` uses 5 ms). At the default, ~100 ms, a quick statement on
25
+ * another connection can wait ~50 ms for a slice to end. It cannot be read
26
+ * back. When the opener knows it (`connectionFrom`'s `pollingIntervalMs`),
27
+ * the lane trusts that; otherwise it measures, and warns once per page when
28
+ * the MEDIAN slice over its first sliced statements runs long. A median, not
29
+ * one reading: each poll waits for a main-thread turn, and during load a busy
30
+ * main thread stretches round trips past the threshold with short slices.
31
+ */
32
+ /** Slices longer than this warn once: the polling interval is long. */
33
+ const SLOW_SLICE_MS = 25;
34
+ /** Enough polls in one statement to measure its slices. */
35
+ const MEASURE_POLLS = 3;
36
+ /** Statements measured before judging: the median of these. */
37
+ const MEASURE_STATEMENTS = 5;
38
+ let warnedSlowSlices = false;
39
+ function abortError() {
40
+ return new DOMException('Statement aborted', 'AbortError');
41
+ }
42
+ export class PendingQueryLane {
43
+ #bindings;
44
+ #conn;
45
+ #closeConnection;
46
+ #queue = [];
47
+ #running = null;
48
+ #runningDone = null;
49
+ #closed = false;
50
+ #hold;
51
+ /**
52
+ * Wait as `hold` asks before a call that executes. Fetches are not held:
53
+ * they move finished bytes, nearly no worker time, and holding them would
54
+ * only delay the slow result. Returns the wait, which is not execution, or
55
+ * null when there is none: then the caller goes on in the same tick, as a
56
+ * lane without a hold always did.
57
+ */
58
+ #beforeSlice() {
59
+ const wait = this.#hold?.();
60
+ if (!wait)
61
+ return null;
62
+ const t = performance.now();
63
+ return wait.then(() => performance.now() - t);
64
+ }
65
+ /** Per-statement slice lengths, until judged; null when not measuring. */
66
+ #slices = [];
67
+ constructor(bindings, conn, closeConnection,
68
+ /** The database's polling interval, when the opener knows it. */
69
+ options = {}) {
70
+ this.#bindings = bindings;
71
+ this.#conn = conn;
72
+ this.#closeConnection = closeConnection;
73
+ this.#hold = options.hold ?? null;
74
+ const known = options.pollingIntervalMs;
75
+ if (known != null) {
76
+ this.#slices = null;
77
+ if (known > SLOW_SLICE_MS)
78
+ warnSlowSlices(known);
79
+ }
80
+ }
81
+ query(sql, signal) {
82
+ return this.#enqueue(sql, signal, true);
83
+ }
84
+ async exec(sql, signal) {
85
+ const { polls } = await this.#enqueue(sql, signal, false);
86
+ return { polls };
87
+ }
88
+ async close() {
89
+ if (this.#closed)
90
+ return;
91
+ this.#closed = true;
92
+ for (const job of this.#queue)
93
+ this.#settleAborted(job);
94
+ this.#queue = [];
95
+ if (this.#running)
96
+ this.#abort(this.#running);
97
+ await this.#runningDone;
98
+ await this.#closeConnection();
99
+ }
100
+ #enqueue(sql, signal, fetch) {
101
+ if (this.#closed || signal?.aborted)
102
+ return Promise.reject(abortError());
103
+ return new Promise((resolve, reject) => {
104
+ const job = {
105
+ sql,
106
+ signal,
107
+ fetch,
108
+ phase: 'queued',
109
+ aborted: false,
110
+ resolve,
111
+ reject,
112
+ onAbort: null
113
+ };
114
+ if (signal) {
115
+ job.onAbort = () => this.#abort(job);
116
+ signal.addEventListener('abort', job.onAbort, { once: true });
117
+ }
118
+ this.#queue.push(job);
119
+ this.#pump();
120
+ });
121
+ }
122
+ #abort(job) {
123
+ if (job.aborted)
124
+ return;
125
+ job.aborted = true;
126
+ if (job.phase === 'queued') {
127
+ const i = this.#queue.indexOf(job);
128
+ if (i >= 0)
129
+ this.#queue.splice(i, 1);
130
+ this.#settleAborted(job);
131
+ }
132
+ else if (job.phase === 'running') {
133
+ // Interrupts execution; the poll in flight rejects or, if the query
134
+ // just finished, returns the header. Either way #run sees `aborted`.
135
+ this.#bindings.cancelPendingQuery(this.#conn).catch(() => { });
136
+ }
137
+ // 'fetching': the query has finished; #run stops at its next fetch.
138
+ }
139
+ #settleAborted(job) {
140
+ job.aborted = true;
141
+ this.#detach(job);
142
+ job.reject(abortError());
143
+ }
144
+ #detach(job) {
145
+ if (job.onAbort)
146
+ job.signal?.removeEventListener('abort', job.onAbort);
147
+ job.onAbort = null;
148
+ }
149
+ #pump() {
150
+ if (this.#running)
151
+ return;
152
+ const job = this.#queue.shift();
153
+ if (!job)
154
+ return;
155
+ this.#running = job;
156
+ this.#runningDone = this.#run(job).finally(() => {
157
+ this.#running = null;
158
+ this.#runningDone = null;
159
+ this.#pump();
160
+ });
161
+ }
162
+ async #run(job) {
163
+ const b = this.#bindings;
164
+ const conn = this.#conn;
165
+ job.phase = 'running';
166
+ const t0 = performance.now();
167
+ let polls = 0;
168
+ let held = 0;
169
+ try {
170
+ let header;
171
+ try {
172
+ const before = this.#beforeSlice();
173
+ if (before)
174
+ held += await before;
175
+ if (job.aborted)
176
+ throw abortError();
177
+ header = await b.startPendingQuery(conn, job.sql, false);
178
+ while (header == null) {
179
+ const wait = this.#beforeSlice();
180
+ if (wait)
181
+ held += await wait;
182
+ if (job.aborted)
183
+ throw abortError();
184
+ polls++;
185
+ header = await b.pollPendingQuery(conn);
186
+ }
187
+ }
188
+ catch (e) {
189
+ // The cancel's own rejection, or anything else after an abort: one
190
+ // outcome for the caller. Not asked for: passed through as is.
191
+ throw job.aborted ? abortError() : e;
192
+ }
193
+ // Finished just as a cancel arrived: aborted all the same.
194
+ if (job.aborted)
195
+ throw abortError();
196
+ // Held time is waiting for the main connection, not execution.
197
+ const execMs = performance.now() - t0 - held;
198
+ const ipc = [header];
199
+ if (job.fetch) {
200
+ job.phase = 'fetching';
201
+ for (;;) {
202
+ if (job.aborted)
203
+ throw abortError();
204
+ const chunk = await b.fetchQueryResults(conn);
205
+ if (!chunk || chunk.byteLength === 0)
206
+ break;
207
+ ipc.push(chunk);
208
+ }
209
+ }
210
+ if (job.aborted)
211
+ throw abortError();
212
+ this.#noteSlices(polls, execMs);
213
+ this.#detach(job);
214
+ job.resolve({ ipc, polls, execMs });
215
+ }
216
+ catch (e) {
217
+ this.#detach(job);
218
+ job.reject(e);
219
+ }
220
+ }
221
+ #noteSlices(polls, ms) {
222
+ const slices = this.#slices;
223
+ if (!slices || polls < MEASURE_POLLS)
224
+ return;
225
+ slices.push(ms / polls);
226
+ if (slices.length < MEASURE_STATEMENTS)
227
+ return;
228
+ this.#slices = null;
229
+ const median = [...slices].sort((x, y) => x - y)[slices.length >> 1];
230
+ if (median > SLOW_SLICE_MS)
231
+ warnSlowSlices(median);
232
+ }
233
+ }
234
+ function warnSlowSlices(ms) {
235
+ if (warnedSlowSlices)
236
+ return;
237
+ warnedSlowSlices = true;
238
+ console.warn(`[Fjader] DuckDB runs sliced statements in ~${Math.round(ms)} ms slices, so a quick ` +
239
+ 'query can wait that long behind a slow one. Open the database with ' +
240
+ 'db.open({ query: { queryPollingInterval: 5 } }) and pass ' +
241
+ 'connectionFrom(conn, { pollingIntervalMs: 5 }) (initDuckDB does both).');
242
+ }
243
+ /** For tests: the once-per-page slice warning, rearmed. */
244
+ export function _resetSliceWarning() {
245
+ warnedSlowSlices = false;
246
+ }
247
+ /**
248
+ * One lane, opened on first use. `get()` answers in the same tick once the
249
+ * lane's state is known (open, or none to be had), and returns a promise only
250
+ * while it opens: a statement sent while there is no lane goes out with no
251
+ * extra microtask, so nothing sent after it in the same tick can overtake it.
252
+ */
253
+ export class LaneSlot {
254
+ #open;
255
+ #onFail;
256
+ #state = 'unknown';
257
+ #lane = null;
258
+ #promise = null;
259
+ /** `open` returns null when no lane can be had (no capability). */
260
+ constructor(open, onFail) {
261
+ this.#open = open;
262
+ this.#onFail = onFail;
263
+ }
264
+ get isOpen() {
265
+ return this.#state === 'open';
266
+ }
267
+ get() {
268
+ if (this.#state === 'open')
269
+ return this.#lane;
270
+ if (this.#state === 'none')
271
+ return null;
272
+ if (this.#promise)
273
+ return this.#promise;
274
+ const opening = this.#open();
275
+ if (!opening) {
276
+ this.#state = 'none';
277
+ return null;
278
+ }
279
+ this.#state = 'opening';
280
+ this.#promise = opening.then((lane) => {
281
+ this.#lane = lane;
282
+ this.#state = 'open';
283
+ return lane;
284
+ }, (e) => {
285
+ this.#state = 'none';
286
+ this.#onFail(e);
287
+ return null;
288
+ });
289
+ return this.#promise;
290
+ }
291
+ /** Close the lane if it was opened (or is opening); none can open after. */
292
+ async close() {
293
+ const lane = this.#promise ? await this.#promise : null;
294
+ this.#state = 'none';
295
+ this.#lane = null;
296
+ if (lane)
297
+ await lane.close().catch(() => { });
298
+ }
299
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Where an interaction read runs, by the source it reads: the main connection
3
+ * (`runQuery`: no polling, no chunked fetch) or a sliced lane, where a slow
4
+ * read interleaves with everyone else's instead of holding the worker.
5
+ *
6
+ * A COST RECORD PER SOURCE (the table a statement actually reads: a base table,
7
+ * a view, or a preagg table): unknown, fast or slow.
8
+ *
9
+ * - Unknown and slow sources read on a lane; fast ones on the main connection.
10
+ * - LANES ARE FOR SLOW SOURCES ONLY. A lane read costs 20-27 ms where the main
11
+ * connection takes 2-5 ms (measured on /test/entrance at load): it needs at
12
+ * least three round trips (start, fetch, a final empty fetch), and each
13
+ * waits behind a slice of every other busy lane, a cost that grows with the
14
+ * number of busy lanes (so the pool stays small).
15
+ * - Hence the PRIOR, where a source starts: first from the DDL that made it
16
+ * through the engine (QueryEngine #noteDDL: a table starts fast, a view
17
+ * unknown), else from the catalog, looked up off the critical path (a
18
+ * table in an in-memory catalog starts fast; anything else unknown). A
19
+ * preagg table, which fjader builds in memory, starts fast. fjader's own
20
+ * identity views are never a source: a mark's reads name its base table.
21
+ * An app can override the prior (QueryEngine.sourceCost), and a
22
+ * measurement corrects either.
23
+ * - TEMP sources never leave the main connection: they exist only on the
24
+ * connection that made it (QueryEngine #localSources).
25
+ * - Only DATA reads update the record. A source's first statements are a
26
+ * DESCRIBE or a range query, quick even when its data query (a GROUP BY, a
27
+ * window, dense padding) is not.
28
+ * - On a lane the measure is the poll count, taken in the worker: finished in
29
+ * its first slice or the next marks the source FAST, whatever its record
30
+ * was. Symmetric on purpose: the usual slow read is a cold one (remote
31
+ * parquet, a view slow the first time), and brushing that source later is
32
+ * quick; a "slow" that stuck would slice every brush frame for the session.
33
+ * - More slices than that marks it SLOW, unless the result was large and its
34
+ * execution short: a 2M-row read passes two 5 ms slices on transfer and
35
+ * decode alone, the fetch phase barely holds the worker, and on a lane it
36
+ * would come back in ~1000 chunks (+~40 ms a frame). Large results stay on
37
+ * the main connection unless execution alone is long.
38
+ * - On the main connection the measure is send-to-bytes time, taken on the
39
+ * main thread, so a busy frame inflates it: a fast source turns slow only
40
+ * after TWO consecutive slow reads.
41
+ *
42
+ * ORDER when a source changes lane. Its next read may go to the main
43
+ * connection while an earlier one is still queued on its lane. Mark data
44
+ * cannot overtake itself: execute-latest keeps at most one read of a mark in
45
+ * flight, and the next waits for it. Setup reads are cached and order-free.
46
+ * A future read without a callerId that cares about order must keep its
47
+ * source's lane.
48
+ */
49
+ export type SourceCost = 'unknown' | 'fast' | 'slow';
50
+ /** Past this many polls (slices after the first) a lane read was slow. */
51
+ export declare const FAST_POLLS = 1;
52
+ /** A main-connection read slower than this, twice running, turns its source slow. */
53
+ export declare const SLOW_MAIN_MS = 50;
54
+ /** A result this large is "large": it stays on the main connection... */
55
+ export declare const LARGE_ROWS = 200000;
56
+ /** ...unless its execution alone takes longer than this. */
57
+ export declare const LARGE_EXEC_MS = 100;
58
+ export declare class SourceRouting {
59
+ #private;
60
+ constructor(laneCount: number);
61
+ cost(source: string): SourceCost;
62
+ /**
63
+ * The prior for a source not yet measured: what it is (see the module
64
+ * comment). Applies only while its record is unknown and not hinted.
65
+ */
66
+ prior(source: string, cost: SourceCost): void;
67
+ /**
68
+ * The source was dropped or replaced by something else: back to unknown,
69
+ * unless the app hinted it.
70
+ */
71
+ forget(source: string): void;
72
+ /** The app knows better than the prior: a slow table, a cheap view. */
73
+ hint(source: string, cost: 'fast' | 'slow'): void;
74
+ /** The lane a source sticks to, whatever its cost (a read forced sliced). */
75
+ laneOf(source: string): number;
76
+ /** The lane index to read `source` on, or null for the main connection. */
77
+ route(source: string): number | null;
78
+ /**
79
+ * A data read finished on a lane: `polls` slices after the first, `execMs`
80
+ * until its result header, `rows` in the result.
81
+ */
82
+ noteLaneRead(source: string, polls: number, execMs: number, rows: number): void;
83
+ /** A data read finished on the main connection, `ms` from send to its bytes. */
84
+ noteMainRead(source: string, ms: number, rows: number): void;
85
+ /**
86
+ * A data reload: every measured cost is unknown again (the prior is set
87
+ * again by the next describe); an app's hint stays.
88
+ */
89
+ reset(): void;
90
+ }
91
+ /**
92
+ * What a statement does to the set of sources, read from its text: a table or
93
+ * view created under a plain name, or one dropped. Narrow on purpose: a wrong
94
+ * "table" (fast) is the expensive mistake, a wrong "unknown" costs one sliced
95
+ * read (~20 ms) before the first quick one corrects it. Qualified names
96
+ * (an attached database), CREATE MACRO, TYPE, SEQUENCE and anything else
97
+ * return null.
98
+ */
99
+ export declare function sourceDDL(sql: string): {
100
+ kind: 'table' | 'view';
101
+ name: string;
102
+ temp: boolean;
103
+ } | {
104
+ kind: 'drop';
105
+ name: string;
106
+ } | null;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Where an interaction read runs, by the source it reads: the main connection
3
+ * (`runQuery`: no polling, no chunked fetch) or a sliced lane, where a slow
4
+ * read interleaves with everyone else's instead of holding the worker.
5
+ *
6
+ * A COST RECORD PER SOURCE (the table a statement actually reads: a base table,
7
+ * a view, or a preagg table): unknown, fast or slow.
8
+ *
9
+ * - Unknown and slow sources read on a lane; fast ones on the main connection.
10
+ * - LANES ARE FOR SLOW SOURCES ONLY. A lane read costs 20-27 ms where the main
11
+ * connection takes 2-5 ms (measured on /test/entrance at load): it needs at
12
+ * least three round trips (start, fetch, a final empty fetch), and each
13
+ * waits behind a slice of every other busy lane, a cost that grows with the
14
+ * number of busy lanes (so the pool stays small).
15
+ * - Hence the PRIOR, where a source starts: first from the DDL that made it
16
+ * through the engine (QueryEngine #noteDDL: a table starts fast, a view
17
+ * unknown), else from the catalog, looked up off the critical path (a
18
+ * table in an in-memory catalog starts fast; anything else unknown). A
19
+ * preagg table, which fjader builds in memory, starts fast. fjader's own
20
+ * identity views are never a source: a mark's reads name its base table.
21
+ * An app can override the prior (QueryEngine.sourceCost), and a
22
+ * measurement corrects either.
23
+ * - TEMP sources never leave the main connection: they exist only on the
24
+ * connection that made it (QueryEngine #localSources).
25
+ * - Only DATA reads update the record. A source's first statements are a
26
+ * DESCRIBE or a range query, quick even when its data query (a GROUP BY, a
27
+ * window, dense padding) is not.
28
+ * - On a lane the measure is the poll count, taken in the worker: finished in
29
+ * its first slice or the next marks the source FAST, whatever its record
30
+ * was. Symmetric on purpose: the usual slow read is a cold one (remote
31
+ * parquet, a view slow the first time), and brushing that source later is
32
+ * quick; a "slow" that stuck would slice every brush frame for the session.
33
+ * - More slices than that marks it SLOW, unless the result was large and its
34
+ * execution short: a 2M-row read passes two 5 ms slices on transfer and
35
+ * decode alone, the fetch phase barely holds the worker, and on a lane it
36
+ * would come back in ~1000 chunks (+~40 ms a frame). Large results stay on
37
+ * the main connection unless execution alone is long.
38
+ * - On the main connection the measure is send-to-bytes time, taken on the
39
+ * main thread, so a busy frame inflates it: a fast source turns slow only
40
+ * after TWO consecutive slow reads.
41
+ *
42
+ * ORDER when a source changes lane. Its next read may go to the main
43
+ * connection while an earlier one is still queued on its lane. Mark data
44
+ * cannot overtake itself: execute-latest keeps at most one read of a mark in
45
+ * flight, and the next waits for it. Setup reads are cached and order-free.
46
+ * A future read without a callerId that cares about order must keep its
47
+ * source's lane.
48
+ */
49
+ /** Past this many polls (slices after the first) a lane read was slow. */
50
+ export const FAST_POLLS = 1;
51
+ /** A main-connection read slower than this, twice running, turns its source slow. */
52
+ export const SLOW_MAIN_MS = 50;
53
+ /** A result this large is "large": it stays on the main connection... */
54
+ export const LARGE_ROWS = 200_000;
55
+ /** ...unless its execution alone takes longer than this. */
56
+ export const LARGE_EXEC_MS = 100;
57
+ export class SourceRouting {
58
+ #records = new Map();
59
+ #laneCount;
60
+ /** Sources per lane, to place a new one on the least used. */
61
+ #laneLoad;
62
+ constructor(laneCount) {
63
+ this.#laneCount = laneCount;
64
+ this.#laneLoad = new Array(laneCount).fill(0);
65
+ }
66
+ #of(source) {
67
+ let r = this.#records.get(source);
68
+ if (!r) {
69
+ let lane = 0;
70
+ for (let i = 1; i < this.#laneCount; i++) {
71
+ if (this.#laneLoad[i] < this.#laneLoad[lane])
72
+ lane = i;
73
+ }
74
+ this.#laneLoad[lane]++;
75
+ r = { cost: 'unknown', hinted: false, mainSlow: 0, lane };
76
+ this.#records.set(source, r);
77
+ }
78
+ return r;
79
+ }
80
+ cost(source) {
81
+ return this.#records.get(source)?.cost ?? 'unknown';
82
+ }
83
+ /**
84
+ * The prior for a source not yet measured: what it is (see the module
85
+ * comment). Applies only while its record is unknown and not hinted.
86
+ */
87
+ prior(source, cost) {
88
+ const r = this.#of(source);
89
+ if (r.cost === 'unknown' && !r.hinted)
90
+ r.cost = cost;
91
+ }
92
+ /**
93
+ * The source was dropped or replaced by something else: back to unknown,
94
+ * unless the app hinted it.
95
+ */
96
+ forget(source) {
97
+ const r = this.#records.get(source);
98
+ if (!r || r.hinted)
99
+ return;
100
+ r.cost = 'unknown';
101
+ r.mainSlow = 0;
102
+ }
103
+ /** The app knows better than the prior: a slow table, a cheap view. */
104
+ hint(source, cost) {
105
+ const r = this.#of(source);
106
+ r.cost = cost;
107
+ r.hinted = true;
108
+ r.mainSlow = 0;
109
+ }
110
+ /** The lane a source sticks to, whatever its cost (a read forced sliced). */
111
+ laneOf(source) {
112
+ return this.#of(source).lane;
113
+ }
114
+ /** The lane index to read `source` on, or null for the main connection. */
115
+ route(source) {
116
+ const r = this.#of(source);
117
+ return r.cost === 'fast' ? null : r.lane;
118
+ }
119
+ /**
120
+ * A data read finished on a lane: `polls` slices after the first, `execMs`
121
+ * until its result header, `rows` in the result.
122
+ */
123
+ noteLaneRead(source, polls, execMs, rows) {
124
+ const r = this.#of(source);
125
+ r.mainSlow = 0;
126
+ if (polls <= FAST_POLLS)
127
+ r.cost = 'fast';
128
+ else if (rows > LARGE_ROWS && execMs <= LARGE_EXEC_MS)
129
+ r.cost = 'fast';
130
+ else
131
+ r.cost = 'slow';
132
+ }
133
+ /** A data read finished on the main connection, `ms` from send to its bytes. */
134
+ noteMainRead(source, ms, rows) {
135
+ const r = this.#of(source);
136
+ const slow = rows > LARGE_ROWS ? ms > LARGE_EXEC_MS : ms > SLOW_MAIN_MS;
137
+ if (!slow) {
138
+ r.mainSlow = 0;
139
+ if (r.cost === 'unknown')
140
+ r.cost = 'fast';
141
+ return;
142
+ }
143
+ if (++r.mainSlow >= 2) {
144
+ r.cost = 'slow';
145
+ r.mainSlow = 0;
146
+ }
147
+ }
148
+ /**
149
+ * A data reload: every measured cost is unknown again (the prior is set
150
+ * again by the next describe); an app's hint stays.
151
+ */
152
+ reset() {
153
+ for (const r of this.#records.values()) {
154
+ if (r.hinted)
155
+ continue;
156
+ r.cost = 'unknown';
157
+ r.mainSlow = 0;
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * What a statement does to the set of sources, read from its text: a table or
163
+ * view created under a plain name, or one dropped. Narrow on purpose: a wrong
164
+ * "table" (fast) is the expensive mistake, a wrong "unknown" costs one sliced
165
+ * read (~20 ms) before the first quick one corrects it. Qualified names
166
+ * (an attached database), CREATE MACRO, TYPE, SEQUENCE and anything else
167
+ * return null.
168
+ */
169
+ export function sourceDDL(sql) {
170
+ const name = String.raw `("(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)`;
171
+ const end = String.raw `(?=\s|\(|;|$)`;
172
+ const create = new RegExp(String.raw `^\s*CREATE\s+(?:OR\s+REPLACE\s+)?((?:TEMP|TEMPORARY)\s+)?(TABLE|VIEW)\s+(?:IF\s+NOT\s+EXISTS\s+)?` +
173
+ name +
174
+ end, 'i').exec(sql);
175
+ if (create)
176
+ return {
177
+ kind: create[2].toUpperCase() === 'TABLE' ? 'table' : 'view',
178
+ name: unquote(create[3]),
179
+ // TEMP objects belong to the connection that made them.
180
+ temp: !!create[1]
181
+ };
182
+ const drop = new RegExp(String.raw `^\s*DROP\s+(?:TABLE|VIEW)\s+(?:IF\s+EXISTS\s+)?` + name + end, 'i').exec(sql);
183
+ if (drop)
184
+ return { kind: 'drop', name: unquote(drop[1]) };
185
+ return null;
186
+ }
187
+ function unquote(name) {
188
+ return name.startsWith('"') ? name.slice(1, -1).replaceAll('""', '"') : name;
189
+ }
@@ -310,7 +310,9 @@ export declare class BandScaleState {
310
310
  * reports what CHANGED rather than notifying anyone: nothing below runs for
311
311
  * an answer identical to the one already on screen.
312
312
  */
313
- updateDomainFromExtents(): void;
313
+ updateDomainFromExtents(options?: {
314
+ instant?: boolean;
315
+ }): void;
314
316
  /**
315
317
  * The band channel as GPU-ready floats.
316
318
  *
@@ -362,6 +364,12 @@ export declare class BandScaleState {
362
364
  * Changes only when the dim or the domain does.
363
365
  */
364
366
  getTicks(): Float32Array;
367
+ /**
368
+ * The slots' latest layout if it landed at once (a catch-up result), else 0:
369
+ * what follows from it, like `tickVisibility`, lands with it. Reactive
370
+ * through `_version`, which every layout bumps.
371
+ */
372
+ get slotsLandedLayout(): number;
365
373
  /**
366
374
  * Where each grid line is headed: 1 for a displayed category, 0 otherwise,
367
375
  * row-aligned with `getTicks`. A grid morphs its lines' opacity toward this on
@@ -451,12 +451,12 @@ export class BandScaleState {
451
451
  * all-NULL column that renders nothing — because an expression-backed axis
452
452
  * gets a dim like any other since `c55e719`.
453
453
  */
454
- #rebuildSlotTable() {
454
+ #rebuildSlotTable(instant = false) {
455
455
  if (this.hasIdentity) {
456
- this.#slots.layout('ids', this.#displayOrder, this.#resolver.idCount);
456
+ this.#slots.layout('ids', this.#displayOrder, this.#resolver.idCount, instant);
457
457
  }
458
458
  else {
459
- this.#slots.layout('positions', null, this.#domain.length);
459
+ this.#slots.layout('positions', null, this.#domain.length, instant);
460
460
  }
461
461
  }
462
462
  // =========================================================================
@@ -552,7 +552,7 @@ export class BandScaleState {
552
552
  * reports what CHANGED rather than notifying anyone: nothing below runs for
553
553
  * an answer identical to the one already on screen.
554
554
  */
555
- updateDomainFromExtents() {
555
+ updateDomainFromExtents(options = {}) {
556
556
  if (!this.#enableUpdates) {
557
557
  return;
558
558
  }
@@ -566,7 +566,9 @@ export class BandScaleState {
566
566
  return;
567
567
  this.#domain = values;
568
568
  this.#displayOrder = ids;
569
- this.#rebuildSlotTable();
569
+ // Instant: the categories take their new slots at once (a catch-up
570
+ // result, see SlotTransition.layout).
571
+ this.#rebuildSlotTable(options.instant);
570
572
  this._version++;
571
573
  // Notify GPU to re-render charts using this scale
572
574
  this.#registry?.invalidateChartsUsingScale(this);
@@ -684,6 +686,15 @@ export class BandScaleState {
684
686
  ticks[i] = i;
685
687
  return ticks;
686
688
  }
689
+ /**
690
+ * The slots' latest layout if it landed at once (a catch-up result), else 0:
691
+ * what follows from it, like `tickVisibility`, lands with it. Reactive
692
+ * through `_version`, which every layout bumps.
693
+ */
694
+ get slotsLandedLayout() {
695
+ void this._version;
696
+ return this.#slots.landedLayout;
697
+ }
687
698
  /**
688
699
  * Where each grid line is headed: 1 for a displayed category, 0 otherwise,
689
700
  * row-aligned with `getTicks`. A grid morphs its lines' opacity toward this on
@@ -1,3 +1,4 @@
1
+ import type { EntranceGate } from '../animation/EntranceGate';
1
2
  import type { RegisteredScaleRequirement, ResolvedScaleConfiguration } from './ScalePriorities';
2
3
  import type { ScaleRegistry } from './ScaleRegistry.svelte';
3
4
  import type { GPUCoordinator } from '../gpu/GPUCoordinator.svelte';
@@ -32,6 +33,8 @@ interface ChartScalesConfig {
32
33
  yDisableZoom?: boolean;
33
34
  useXSelection?: boolean;
34
35
  useYSelection?: boolean;
36
+ /** The chart's entrance gate: its scales snap, and show no ticks, until it opens. */
37
+ entranceGate?: EntranceGate | null;
35
38
  }
36
39
  export declare class ChartScales {
37
40
  #private;