@kanzo-tech/graph 0.1.0 → 0.3.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/README.md +15 -7
- package/dist/bounded.d.ts +119 -69
- package/dist/bounded.d.ts.map +1 -1
- package/dist/bounded.js +13 -34
- package/dist/bounded.js.map +1 -1
- package/dist/duck-source.d.ts +60 -72
- package/dist/duck-source.d.ts.map +1 -1
- package/dist/duck-source.js +185 -255
- package/dist/duck-source.js.map +1 -1
- package/dist/graph-canvas.d.ts +1 -1
- package/dist/graph-canvas.js.map +1 -1
- package/dist/graph-looks.d.ts +63 -18
- package/dist/graph-looks.d.ts.map +1 -1
- package/dist/graph-looks.js +37 -26
- package/dist/graph-looks.js.map +1 -1
- package/dist/graph-model.d.ts +2 -2
- package/dist/graph-model.d.ts.map +1 -1
- package/dist/graph-model.js +19 -14
- package/dist/graph-model.js.map +1 -1
- package/dist/graph-sim.d.ts +15 -1
- package/dist/graph-sim.d.ts.map +1 -1
- package/dist/graph-sim.js +17 -13
- package/dist/graph-sim.js.map +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -48
- package/dist/index.js.map +1 -1
- package/dist/obligations.d.ts +1 -3
- package/dist/obligations.d.ts.map +1 -1
- package/dist/shape-glyph.d.ts +30 -0
- package/dist/shape-glyph.d.ts.map +1 -0
- package/dist/shape-glyph.js +15 -0
- package/dist/shape-glyph.js.map +1 -0
- package/dist/slice-client.d.ts.map +1 -1
- package/dist/slice-client.js +36 -36
- package/dist/slice-client.js.map +1 -1
- package/dist/use-graph-overlays.d.ts +21 -30
- package/dist/use-graph-overlays.d.ts.map +1 -1
- package/dist/use-graph-overlays.js +39 -37
- package/dist/use-graph-overlays.js.map +1 -1
- package/dist/use-graph.d.ts +38 -12
- package/dist/use-graph.d.ts.map +1 -1
- package/dist/use-graph.js +62 -61
- package/dist/use-graph.js.map +1 -1
- package/dist/use-query-loop.d.ts +8 -4
- package/dist/use-query-loop.d.ts.map +1 -1
- package/dist/use-query-loop.js +93 -96
- package/dist/use-query-loop.js.map +1 -1
- package/dist/use-renderer.d.ts.map +1 -1
- package/dist/use-renderer.js +69 -66
- package/dist/use-renderer.js.map +1 -1
- package/package.json +11 -5
- package/dist/memory-source.d.ts +0 -32
- package/dist/memory-source.d.ts.map +0 -1
- package/dist/memory-source.js +0 -134
- package/dist/memory-source.js.map +0 -1
package/README.md
CHANGED
|
@@ -28,8 +28,15 @@ pnpm add @kanzo-tech/graph @cosmos.gl/graph
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`@cosmos.gl/graph` is a required peer: this is a renderer, and there is nothing left of it without
|
|
31
|
-
one. The Mosaic peers are **optional** — `
|
|
32
|
-
rendering hooks do not
|
|
31
|
+
one. The Mosaic peers and fossil's corpus reader are **optional** — `openCorpus()` needs them and
|
|
32
|
+
the rendering hooks do not.
|
|
33
|
+
|
|
34
|
+
**So the root barrel draws nothing, and that is the shape rather than an oversight.** The reason for
|
|
35
|
+
the split used to be *a host drawing arrays it already has should not pay for a database*, and that
|
|
36
|
+
host no longer exists here: `memorySource` is deleted and this package sends **one** source, the
|
|
37
|
+
corpus'. What the split protects now is a barrel that is a rendering surface — hooks, looks,
|
|
38
|
+
identity, the buffers a slice implies — for a host whose slices arrive from somewhere else, its own
|
|
39
|
+
`BoundedSource` included. A picture costs `@kanzo-tech/graph/duckdb`.
|
|
33
40
|
|
|
34
41
|
## The two halves
|
|
35
42
|
|
|
@@ -40,11 +47,12 @@ is **sampled** rather than truncated — one row every `ceil(matched / limit)` o
|
|
|
40
47
|
Morton-ordered `dense_id`, which spreads the marks over the window instead of drawing a corner of
|
|
41
48
|
it. So a view of everything is still a few thousand marks, and they are still everywhere.
|
|
42
49
|
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
a
|
|
50
|
+
`openCorpus` — on `@kanzo-tech/graph/duckdb`, because that is the half that needs Mosaic and
|
|
51
|
+
fossil's reader — takes where a corpus is and hands back both halves: the source the canvas draws
|
|
52
|
+
from, and the relations registered under their own names for the charts, the crossfilter and the
|
|
53
|
+
verbs. It takes no column names and no type index; those come off the manifest or they do not come.
|
|
54
|
+
Under `limit` it is asked once for everything and never again, so a corpus that fits pays for
|
|
55
|
+
nothing.
|
|
48
56
|
|
|
49
57
|
**A point is addressed by index and identified by pair.** cosmos.gl numbers points by their position
|
|
50
58
|
in the arrays it was last handed, so index 7 is whatever the current answer put seventh. A vertex is
|
package/dist/bounded.d.ts
CHANGED
|
@@ -102,20 +102,15 @@ export interface Slice {
|
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
104
|
* A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a
|
|
105
|
-
* reader panning across a laid-out corpus, and it is what
|
|
105
|
+
* reader panning across a laid-out corpus, and it is what a corpus can answer by address.
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
* A network has no spatial "near"; it has topological near, and a rectangle cannot express
|
|
109
|
-
* from this node" no matter how it is positioned
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* `supports(kind)` predicate, and a `throw` at the top of `slice`. Three spellings of one idea, and
|
|
115
|
-
* the only one a caller could act on before making the call was the middle one — so asking a
|
|
116
|
-
* relational source for a neighbourhood was a runtime error that typechecked. It is a separate,
|
|
117
|
-
* optional method now: a source that cannot walk edges does not have it, and asking is a compile
|
|
118
|
-
* error rather than a promise that rejects.
|
|
107
|
+
* **It is the only question this contract asks, and that is a narrowing rather than the natural
|
|
108
|
+
* shape.** A network has no spatial "near"; it has topological near, and a rectangle cannot express
|
|
109
|
+
* "two hops from this node" no matter how it is positioned — so a contract that only speaks
|
|
110
|
+
* rectangles imposes the map metaphor on a network. `ExploringSource` said the second question here
|
|
111
|
+
* and is deleted with the source that answered it; `index.test.ts` carries the tombstone and the
|
|
112
|
+
* seam it named. What it comes back as is fossil's `expand`, addressed rather than joined — not a
|
|
113
|
+
* second variant of this call.
|
|
119
114
|
*/
|
|
120
115
|
export interface SliceRequest {
|
|
121
116
|
/** The rectangle. */
|
|
@@ -157,8 +152,29 @@ export interface SliceRequest {
|
|
|
157
152
|
* **Above it a source samples the window; it does not take the front of it.** Which is the second
|
|
158
153
|
* half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of
|
|
159
154
|
* those rather than whichever ones an `ORDER BY` happened to put first.
|
|
155
|
+
*
|
|
156
|
+
* Required, and always filled: a host's `limit` is optional all the way down to `useQueryLoop`,
|
|
157
|
+
* which resolves it before the question leaves. A source never has to know what this package
|
|
158
|
+
* would have chosen.
|
|
160
159
|
*/
|
|
161
160
|
limit: number;
|
|
161
|
+
/**
|
|
162
|
+
* The shortest edge worth a row, **in screen pixels** — multiply by `perPixel` for a length in the
|
|
163
|
+
* graph's own space.
|
|
164
|
+
*
|
|
165
|
+
* **What it buys is on `perPixel` below**: two of every three edges a five-million-node window
|
|
166
|
+
* draws are under one pixel long, and discarding everything under three sends a third of the rows
|
|
167
|
+
* for an identical picture.
|
|
168
|
+
*
|
|
169
|
+
* Required for the reason `limit` is, and it is the field this whole shape exists for. It used to
|
|
170
|
+
* live on an exported `BOUNDED_DEFAULTS` that a source read to find out what it was being asked —
|
|
171
|
+
* so a request arrived incomplete and every source finished it in its own words. There were two,
|
|
172
|
+
* one of them in another repository. Now the loop resolves it and a source reads it here.
|
|
173
|
+
*
|
|
174
|
+
* Meaningless without `perPixel`, and a source with no resolution discards nothing: a threshold in
|
|
175
|
+
* pixels with no pixels is not a threshold.
|
|
176
|
+
*/
|
|
177
|
+
minLinkPixels: number;
|
|
162
178
|
/**
|
|
163
179
|
* How much of the graph's own space one screen pixel covers — the resolution the answer is going
|
|
164
180
|
* to be looked at.
|
|
@@ -179,6 +195,33 @@ export interface SliceRequest {
|
|
|
179
195
|
* with no pixels is not a threshold.
|
|
180
196
|
*/
|
|
181
197
|
perPixel?: number;
|
|
198
|
+
/**
|
|
199
|
+
* Stop: the caller does not want this answer any more.
|
|
200
|
+
*
|
|
201
|
+
* **A cancelled question rejects with `signal.reason`, and that is the whole contract.** The
|
|
202
|
+
* camera moves faster than a database answers, so a source that can only hold one question at a
|
|
203
|
+
* time is routinely asked a second before the first has landed. The first caller is still holding
|
|
204
|
+
* a promise; dropping it leaves that caller's `finally` unrun, which in `useQueryLoop` reads as a
|
|
205
|
+
* query permanently in flight for the rest of the session. So the stale promise is **settled**,
|
|
206
|
+
* and this says with what.
|
|
207
|
+
*
|
|
208
|
+
* `AbortController.abort()` puts a `DOMException` named `AbortError` in `reason`, and `throw
|
|
209
|
+
* signal.reason` is the whole implementation. A source that cancels for its own reasons — one
|
|
210
|
+
* standing question re-aimed by a newer caller, a connection let go — throws an `AbortError` it
|
|
211
|
+
* builds itself, which is what `abortError()` below is for.
|
|
212
|
+
*
|
|
213
|
+
* **This replaced a sentinel of ours, and the argument for the sentinel was real.** `SUPERSEDED`
|
|
214
|
+
* was an exported `Symbol` with `isSuperseded` beside it, on the reasoning that *you moved on* and
|
|
215
|
+
* *the database said no* are the two things a query loop must tell apart, and a string comparison
|
|
216
|
+
* against a thrown value goes stale with nothing failing. All of that is true and none of it is an
|
|
217
|
+
* argument for a *private* sentinel: `AbortError` is the name the platform already gives that
|
|
218
|
+
* distinction, `fetch` rejects with it, and every third-party async primitive a source is written
|
|
219
|
+
* over — `fetch`, `AbortSignal.timeout`, a WHATWG stream — produces one without being told to.
|
|
220
|
+
* Ours meant a source had to import a symbol from us to be cancellable at all, and a source that
|
|
221
|
+
* simply passed the signal to `fetch` did the standard thing and was reported to the host as a
|
|
222
|
+
* failure. The comparison is against `error.name`, which is the platform's contract rather than a
|
|
223
|
+
* message.
|
|
224
|
+
*/
|
|
182
225
|
signal?: AbortSignal;
|
|
183
226
|
}
|
|
184
227
|
/**
|
|
@@ -238,39 +281,33 @@ export interface BoundedSource {
|
|
|
238
281
|
watch?(answered: (slice: Slice) => void): () => void;
|
|
239
282
|
}
|
|
240
283
|
/**
|
|
241
|
-
*
|
|
284
|
+
* A cancellation this source is raising itself, in the platform's own shape.
|
|
242
285
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
286
|
+
* For the case a `signal` cannot express: a source holding **one** standing question, re-aimed by a
|
|
287
|
+
* newer caller. There is no signal for the caller that lost — its request was never aborted, it was
|
|
288
|
+
* simply overtaken — so the source builds the rejection, and it builds the same kind the platform
|
|
289
|
+
* would have. `message` says what overtook it; `name` is what anybody tests.
|
|
247
290
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
291
|
+
* Not on the barrel. A source outside this package cancels by passing the `signal` it was handed to
|
|
292
|
+
* whatever it is waiting on, or by `throw signal.reason` — both of which produce an `AbortError`
|
|
293
|
+
* with nothing imported from us. This exists for the one case that has no signal to reach for, and
|
|
294
|
+
* `new DOMException(message, "AbortError")` is the whole of it if a third source ever needs it too.
|
|
251
295
|
*/
|
|
252
|
-
export declare
|
|
253
|
-
export declare function isSuperseded(error: unknown): boolean;
|
|
296
|
+
export declare function abortError(message: string): DOMException;
|
|
254
297
|
/**
|
|
255
|
-
*
|
|
298
|
+
* Whether a rejection means *you moved on* rather than *the answer failed*.
|
|
256
299
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
300
|
+
* `error.name` and not `instanceof`: the same `AbortError` reaches here as a `DOMException` from
|
|
301
|
+
* `AbortController`, as one of ours from `abortError`, and — for a source written over `fetch` in
|
|
302
|
+
* another realm, an iframe or a worker — as an object no `instanceof` in this realm matches. The
|
|
303
|
+
* name is what WHATWG specifies and what every producer agrees on.
|
|
261
304
|
*
|
|
262
|
-
*
|
|
305
|
+
* Not on the barrel either, for the same reason as `abortError`: a caller of this package awaits
|
|
306
|
+
* `slice()` behind an `AbortController` it owns, so `controller.signal.aborted` already answers the
|
|
307
|
+
* question for it. This is the branch `useQueryLoop` needs for the *other* half — a source that
|
|
308
|
+
* cancelled a question the loop had not aborted.
|
|
263
309
|
*/
|
|
264
|
-
export
|
|
265
|
-
explore(request: ExploreRequest): Promise<Slice>;
|
|
266
|
-
}
|
|
267
|
-
/** Where to start and how far out. Everything else is the same bounding as a region. */
|
|
268
|
-
export interface ExploreRequest extends Omit<SliceRequest, "view"> {
|
|
269
|
-
/** Where to start, as identities — not buffer indices, which do not survive an answer. */
|
|
270
|
-
seeds: VertexId[];
|
|
271
|
-
/** How many hops out. One is the ego network; beyond three is usually the whole graph. */
|
|
272
|
-
depth: number;
|
|
273
|
-
}
|
|
310
|
+
export declare function isAbort(error: unknown): boolean;
|
|
274
311
|
/**
|
|
275
312
|
* Whether this graph should be sliced at all.
|
|
276
313
|
*
|
|
@@ -279,36 +316,49 @@ export interface ExploreRequest extends Omit<SliceRequest, "view"> {
|
|
|
279
316
|
*/
|
|
280
317
|
export declare function shouldSlice(total: number | undefined, limit: number): boolean;
|
|
281
318
|
/**
|
|
282
|
-
*
|
|
319
|
+
* What a question means when the host says nothing — **resolved before a source ever sees it.**
|
|
320
|
+
*
|
|
321
|
+
* These were `BOUNDED_DEFAULTS`, an exported table, and a source read it to find out what it was
|
|
322
|
+
* being asked. That is the defect, stated plainly: a request that arrives unresolved is an
|
|
323
|
+
* *incomplete* request, and every source outside this package had to know a constant of ours to
|
|
324
|
+
* finish it. Two of them did — `duck-source.ts` here, and fossil's tile reader — and each finished
|
|
325
|
+
* it in its own words, which is two chances to disagree about one number.
|
|
283
326
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
327
|
+
* `useQueryLoop` fills both in on every call now, so `limit` and `minLinkPixels` are **required**
|
|
328
|
+
* fields of `SliceRequest`: a source reads `request.limit` and is done. The numbers stay module
|
|
329
|
+
* scoped and off the barrel, because nobody outside needs to look them up any more.
|
|
330
|
+
*
|
|
331
|
+
* The camera→rectangle conversion that used to live beside them is gone for the sibling reason:
|
|
332
|
+
* cosmos.gl owns the screen↔space transform and answers it through `screenToSpacePosition`, so
|
|
333
|
+
* deriving the rectangle from the camera and the space size was a second implementation of the
|
|
334
|
+
* renderer's own maths, free to drift from it. `useQueryLoop` asks the renderer instead.
|
|
288
335
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Twenty thousand marks.
|
|
338
|
+
*
|
|
339
|
+
* Above about 50,000 points a live layout stops being comfortable and the edge layer is already
|
|
340
|
+
* fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is
|
|
341
|
+
* the legibility ceiling, which arrives first and is the one a reader actually meets.
|
|
342
|
+
*/
|
|
343
|
+
export declare const DEFAULT_LIMIT = 20000;
|
|
344
|
+
/**
|
|
345
|
+
* The shortest edge worth a row — three screen pixels.
|
|
346
|
+
*
|
|
347
|
+
* Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px
|
|
348
|
+
* at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge
|
|
349
|
+
* that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px
|
|
350
|
+
* sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is
|
|
351
|
+
* in `.planning/FAR-VIEW-AND-EDGES.md`.
|
|
352
|
+
*
|
|
353
|
+
* Three rather than two because both were measured against the same five windows of
|
|
354
|
+
* `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of
|
|
355
|
+
* image.
|
|
356
|
+
*
|
|
357
|
+
* **It is on `SliceRequest` now, and the argument against that has been overtaken.** It used to say:
|
|
358
|
+
* one call site, and a knob with one call site is a knob nobody has an opinion about. There were
|
|
359
|
+
* two call sites by then and one of them was in another repository, both reading the constant off
|
|
360
|
+
* the barrel to reconstruct the same product. Whether it is a *knob* is still open — no caller
|
|
361
|
+
* overrides it — but it is a **fact about the question**, and a question carries its own facts.
|
|
362
|
+
*/
|
|
363
|
+
export declare const DEFAULT_MIN_LINK_PIXELS = 3;
|
|
314
364
|
//# sourceMappingURL=bounded.d.ts.map
|
package/dist/bounded.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8FAA8F;IAC9F,SAAS,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,EAAE,YAAY,CAAC;IACpB,8CAA8C;IAC9C,UAAU,EAAE,WAAW,CAAC;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED
|
|
1
|
+
{"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8FAA8F;IAC9F,SAAS,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,EAAE,YAAY,CAAC;IACpB,8CAA8C;IAC9C,UAAU,EAAE,WAAW,CAAC;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;OAeG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAExD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC"}
|
package/dist/bounded.js
CHANGED
|
@@ -1,39 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return e === n;
|
|
1
|
+
function r(n) {
|
|
2
|
+
return new DOMException(n, "AbortError");
|
|
4
3
|
}
|
|
5
|
-
function
|
|
6
|
-
return
|
|
4
|
+
function t(n) {
|
|
5
|
+
return typeof n == "object" && n !== null && n.name === "AbortError";
|
|
7
6
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Above about 50,000 points a live layout stops being comfortable and the edge layer is already
|
|
13
|
-
* fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is
|
|
14
|
-
* the legibility ceiling, which arrives first and is the one a reader actually meets.
|
|
15
|
-
*/
|
|
16
|
-
limit: 2e4,
|
|
17
|
-
/**
|
|
18
|
-
* The shortest edge worth a row — three screen pixels.
|
|
19
|
-
*
|
|
20
|
-
* Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px
|
|
21
|
-
* at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge
|
|
22
|
-
* that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px
|
|
23
|
-
* sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is
|
|
24
|
-
* in `.planning/FAR-VIEW-AND-EDGES.md`.
|
|
25
|
-
*
|
|
26
|
-
* Three rather than two because both were measured against the same five windows of
|
|
27
|
-
* `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of
|
|
28
|
-
* image. Not on `SliceRequest`, because there is one call site and a knob with one call site is a
|
|
29
|
-
* knob nobody has an opinion about — it moves when a second reader disagrees with the measurement.
|
|
30
|
-
*/
|
|
31
|
-
minLinkPixels: 3
|
|
32
|
-
};
|
|
7
|
+
function e(n, o) {
|
|
8
|
+
return n === void 0 || n > o;
|
|
9
|
+
}
|
|
10
|
+
const c = 2e4, u = 3;
|
|
33
11
|
export {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
c as DEFAULT_LIMIT,
|
|
13
|
+
u as DEFAULT_MIN_LINK_PIXELS,
|
|
14
|
+
r as abortError,
|
|
15
|
+
t as isAbort,
|
|
16
|
+
e as shouldSlice
|
|
38
17
|
};
|
|
39
18
|
//# sourceMappingURL=bounded.js.map
|
package/dist/bounded.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded.js","sources":["../src/bounded.ts"],"sourcesContent":["/**\n * A graph you never hold all of.\n *\n * `load()` is the other kind: it reads a relation whole, turns it into typed arrays, and hands the\n * lot to the renderer. Measured, that costs 389 ms at 200,000 nodes and stops being viable somewhere\n * short of a million — the working set is N, so the ceiling is whatever N the machine can hold.\n *\n * This is the shape that has no such ceiling: something asks a bounded question — a rectangle, or a\n * neighbourhood a few hops wide — and the source answers with **at most `limit` points**. The\n * answer's size follows the question rather than the corpus. Moving re-asks. A window holding more\n * than `limit` is *sampled* rather than truncated, so a view of everything is still a few thousand\n * marks and they are still spread over everything — see `sampled` in `duck-source.ts`.\n *\n * **Positions are authority, not suggestion.** The corpus is written once and read many — OLAP, which\n * is what GraphAr is for — so the coordinates the batch emits are the index every spatial question\n * is asked against. That settles a tension an earlier draft of this file waved at rather than\n * resolved: re-laying-out a slice live would move points out from under the very coordinates the\n * next query is expressed in, and the camera would drift away from the index within one frame of\n * the first force. **Do not re-lay-out a slice.** If a layout is wrong, it is wrong upstream, and it\n * is fixed by recompiling — the graph is a compiler's output and so is its geometry.\n *\n * **Dragging is the exception, and it is a local overlay.** A reader can move a node; that changes\n * where it is *drawn*, never where it is *indexed*. The consequence is small and real: drag a node\n * far away, pan to where you dropped it, and the spatial query does not know it is there. Which is\n * why `pinned` exists below — the few points a reader has taken hold of ride along with every\n * slice, regardless of the rectangle.\n *\n * **Deliberately not a format.** A source is anything that can answer that question: Parquet\n * fetched by address, a plain relation with `x`/`y` columns and a spatial predicate, or an\n * in-memory index. This package renders; it does not learn a storage layout. The\n * wiring between a particular source and this contract belongs at the call site.\n *\n * **Dense indices, not database ids.** `links` refers to positions in `positions`, so a consumer\n * never pays for an id→index map — the 148 ms that mapping costs at 200,000 nodes is not optimised\n * here, it is designed away. Sources that already number their vertices densely (GraphAr's\n * `dense_id` does) hand this over for free.\n */\n\nimport type { VertexId } from \"./resident\";\n\n/**\n * What the camera is looking at, in the graph's own coordinate space.\n *\n * A rectangle and nothing else. It carried a `zoom` for one reader — the level-of-detail threshold\n * a source compared it against to decide whether to answer with super-nodes — and that branch is\n * gone, so the field went with it rather than staying as a number every caller has to invent and\n * nothing reads.\n */\nexport interface Viewport {\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/**\n * One answer. Every array is parallel and indexed densely from zero.\n *\n * `n` is what *matched*, before `limit` cut it — the difference between the two is how a view says\n * \"there is more here than I am showing you\", which is the one honest thing a bounded renderer owes\n * its reader. When a window holds more than `limit`, what comes back is a **sample** of it rather\n * than its first `limit` rows; `n` reports the window either way.\n *\n * **A struct, and it used to be a tagged union.** `mode` picked between points and super-nodes and\n * only the second branch carried `weights`. Both are gone — see `/docs/design/graph` — and with one\n * branch left a discriminant is a\n * field with one legal value.\n */\nexport interface Slice {\n n: number;\n /**\n * How many of `positions` are **marks** — points a reader can see. Everything past it is an\n * *anchor*.\n *\n * An anchor is a real vertex at its real coordinates that the window did not return: it is in the\n * buffers so that an edge leaving the window has somewhere to end. It is never drawn — `buffers`\n * gives it radius zero and alpha zero, and `residentOf` stops here, so nothing hovers, selects or\n * frames one.\n *\n * **Why the far end is the vertex rather than a point on the border.** A stub clipped to the\n * viewport carries the right direction and *lies about the distance*, and a reader cannot tell a\n * stub that ends 1.1 window-widths out from one that ends 47 — measured over all the far ends a\n * window loses, 20–22% are past four semi-widths and the worst is 47.1\n * (`.planning/FAR-VIEW-AND-EDGES.md`). Drawing the vertex where it is cannot lie, and it is also\n * the cheaper of the two: a clipped stub is one point *per edge*, an anchor is one point per far\n * *vertex*.\n *\n * Equal to `positions.length / 2` for a source that answers with marks only, which is why it is\n * required rather than optional — a caller that reads it always gets the count it meant.\n */\n marks: number;\n /**\n * Who each returned point *is*, parallel to `positions` — the `(type_idx, dense_id)` pair packed\n * by `vertexId`.\n *\n * Present because a buffer index is **not stable across answers**: index 7 is a different vertex\n * after a pan. Anything that outlives one answer — a selection, a focused node, a pinned set — has\n * to be held as an identity and re-resolved through `residentOf` each time. Leaving this out was\n * how the first draft would have shipped a selection that silently pointed at the wrong nodes.\n *\n * `BigUint64Array` because the pair is 64 bits exactly — a `Uint32Array` cannot hold it at all,\n * and a `Float64Array` holds it only while the type index stays under 2²¹, which is a ceiling\n * nobody would find until they crossed it. Still a typed array, so it is still one allocation and\n * still transferable; only what it carries changed. A dense id on its own is not an identity: it\n * numbers within one vertex type, so a union of two types repeats every value.\n */\n vertices: BigUint64Array;\n /**\n * The subject IRI of each returned point, parallel to `vertices` — **opt-in, and absent by\n * default.**\n *\n * `vertices` says where a point *is*; this says which vertex it *is*. They are not the same thing\n * and the corpus is explicit about it: redoing a layout renumbers every vertex, so a `dense_id`\n * held outside the corpus names a different vertex after the next write. Anything that has to\n * survive a recompile — a bookmark, a link out, a row in somebody else's database — keys on the\n * IRI. A selection held as `VertexId` survives a pan and does not survive a rebuild.\n *\n * **Absent by default because it costs 1.87× the tile, measured on the corpus side.** Compressed\n * bytes per row at five million: `subject` 8.016 against `dense_id` 4.000, `x` 2.717, `y` 2.501.\n * The four drawing columns are 9.23 B/row and become 17.25 with it. So the drawing path carries\n * addresses, and a host asks for names when something has to be *named* rather than painted.\n *\n * A `string[]` rather than a typed array, because that is what an IRI is. It is the one thing in a\n * `Slice` that does not go to the GPU, which is exactly why it is optional: a host that never\n * names a vertex should not pay to move it.\n */\n subjects?: string[];\n /** `[x0, y0, x1, y1, …]`, one pair per returned point — `marks` of them, then the anchors. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /** Per-point category ordinal, for colour. */\n categories: Uint16Array;\n /**\n * What the size ramp is spent on, per point — a degree, a count, whatever the corpus ranks by.\n *\n * Optional because a source may not have one, and a graph drawn at one radius is a legitimate\n * picture. But without it a look's `form.size` range has only one end, so a source that can afford\n * the column should send it: it is the difference between seeing a hub and counting one.\n */\n sizes?: Float32Array;\n}\n\n/**\n * A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a\n * reader panning across a laid-out corpus, and it is what every source can answer.\n *\n * A **neighbourhood** is the graph question, and it lives on [`ExploringSource`] rather than here.\n * A network has no spatial \"near\"; it has topological near, and a rectangle cannot express \"two hops\n * from this node\" no matter how it is positioned. Leaving it out of the render contract was a real\n * design error — a contract that only spoke rectangles imposed a map metaphor on a network — and\n * putting it back as a *variant of the same call* was a second one, which is what this shape fixes.\n *\n * **The three ways a source used to say \"not that question\".** A member of a query union, a\n * `supports(kind)` predicate, and a `throw` at the top of `slice`. Three spellings of one idea, and\n * the only one a caller could act on before making the call was the middle one — so asking a\n * relational source for a neighbourhood was a runtime error that typechecked. It is a separate,\n * optional method now: a source that cannot walk edges does not have it, and asking is a compile\n * error rather than a promise that rejects.\n */\nexport interface SliceRequest {\n /** The rectangle. */\n view: Viewport;\n /**\n * Which column colours a point — Plot's channel name, and Plot's meaning.\n *\n * **On the request rather than on the source, and that is the whole shape.** A source says *where\n * the bytes are*; a request says *what I want to draw*, and which column colours is the second.\n * Baked into a source at construction — which is where it used to live — changing what a graph is\n * coloured by meant building a new source, and the two are not the same question.\n *\n * It is the reference's own arrangement: in Plot the **mark** carries the channels and the mark is\n * what produces the query, while the data source only says where rows come from.\n *\n * Defaults to `community`, which every corpus has because the layout pass writes it.\n */\n fill?: string;\n /**\n * Which column the size ramp is spent on — Plot's `r`.\n *\n * Omitted, every point is drawn at one radius, which is a legitimate picture: without a ramp a\n * look's `form.size` range has only one end.\n */\n r?: string;\n /**\n * Vertices that must come back whatever the query says.\n *\n * The set a reader has taken hold of — dragged, pinned, selected, focused. Their drawn positions\n * are a view-local overlay on coordinates that never move, so the index cannot find them where\n * they now appear. Carrying them explicitly is cheaper and more honest than making the index\n * mutable: it is a handful of identities, and the alternative is a spatial structure that has to be\n * rewritten every time somebody drags something.\n */\n pinned?: VertexId[];\n /**\n * The most points the source may return.\n *\n * **Above it a source samples the window; it does not take the front of it.** Which is the second\n * half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of\n * those rather than whichever ones an `ORDER BY` happened to put first.\n */\n limit: number;\n /**\n * How much of the graph's own space one screen pixel covers — the resolution the answer is going\n * to be looked at.\n *\n * **What it buys: an edge shorter than three pixels is not sent.** Two of every three edges a\n * five-million-node window draws are under one pixel long — they are a dot on top of their own\n * endpoints, which the point layer has already drawn. Discarding everything under 3 px sends\n * 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical\n * (`.planning/FAR-VIEW-AND-EDGES.md`, measured 2026-08-17).\n *\n * **A row discard, not a fade.** cosmos.gl's `linkVisibilityDistanceRange` already dims a short\n * link, and dimming happens after the row has been joined, returned, uploaded and rasterised. This\n * is the same picture without the work.\n *\n * The rectangle alone cannot say it: the same rectangle over a 400-pixel canvas and a 4,000-pixel\n * one are different questions, and only the caller knows which. Omitted — a source is asked for\n * everything, or by something with no canvas — nothing is discarded, because a threshold in pixels\n * with no pixels is not a threshold.\n */\n perPixel?: number;\n signal?: AbortSignal;\n}\n\n/**\n * Anything that can answer \"what is in this rectangle, at this zoom, in at most this many marks\".\n *\n * One method, one question. A source that also wants search, aggregation or paths is describing a\n * query surface rather than a render path, and there is already one of those — fossil's verbs. The\n * line: this answers *what should I draw*, and nothing about *what does it mean*.\n */\nexport interface BoundedSource {\n slice(request: SliceRequest): Promise<Slice>;\n /**\n * How many vertices there are in total, if the source knows cheaply.\n *\n * The reason this exists is the one real cost of bounding: **a graph that fits pays for nothing.**\n * Unbounded loads once and then pans on the GPU for free; bounded issues a query per camera move.\n * At 200,000 nodes that trade is overwhelmingly worth it — 1,017 ms of first paint against roughly\n * 130 ms — but at 2,000 it is pure loss, tens of milliseconds of latency per pan buying a ceiling\n * nobody was near.\n *\n * So a consumer asks first. Under the limit, take one slice covering everything and never ask\n * again: same code path, and panning is free exactly when it can be.\n */\n total?(): Promise<number>;\n /**\n * The rectangle the corpus occupies, when the source can say cheaply.\n *\n * **Framing the opening view is not the host's job, and treating it as one is measured.** The\n * archive occupies `x ∈ [1843, 2253]` of a 4,096-wide space — 1% of its area — and a camera that\n * opens on the space rather than on the data draws 1,543 points into a tenth of the viewport, at\n * two to nine pixels each, under a fog of links. Every point is uploaded and none is legible, which\n * a reader reports as *the nodes are not rendering*.\n *\n * It matters more for a bounded source than for a whole one: the first question a sliced graph\n * asks is *what is the camera over*, so a camera pointing at empty space is a first paint of\n * nothing. Framing before asking is the difference between one query and none.\n *\n * Optional, because a source over an unlaid-out relation has no answer — and cheap where it\n * exists: a corpus reads it off tile footers it was going to read anyway, and a relation with\n * `x`/`y` gets it from four aggregates.\n */\n extent?(): Promise<Viewport>;\n /**\n * Say when the answer changes for a reason the camera cannot see, and hold the source's resources\n * for as long as anybody is listening.\n *\n * **A whole slice rather than a nudge to ask again, and that is what makes it worth having.** The\n * reason a bounded answer changes on its own is that the page filtered something — somebody\n * brushed a histogram, a panel picked a value — and a source that lives inside a crossfilter is\n * *told* by the coordinator, which has already re-run the reads with the new predicate by the time\n * this fires. Asking again would issue the same two queries a second time to learn what is in hand.\n *\n * The returned function is also the release: it is where a source lets go of whatever it holds —\n * a client registration, a connection, a cache — so a loop that calls this is a loop that cannot\n * leak one. Optional, because a source over arrays holds nothing and changes for nothing.\n */\n watch?(answered: (slice: Slice) => void): () => void;\n}\n\n/**\n * What a superseded question rejects with.\n *\n * A source may answer one question at a time — a shared connection, one client, one in-flight read —\n * so a camera that moves faster than the database answers leaves a promise with a caller awaiting\n * it. Dropping it leaves that caller's `finally` unrun and the loop reporting a query in flight for\n * the rest of the session, so it is *settled*, and this is what with.\n *\n * **A caller treats it as its own abort, never as a failure.** A sentinel rather than a message,\n * because \"you moved on\" and \"the database said no\" are the two things a query loop must tell apart,\n * and a string comparison against a thrown value goes stale with nothing failing.\n */\nexport const SUPERSEDED = Symbol(\"superseded\");\n\nexport function isSuperseded(error: unknown): boolean {\n return error === SUPERSEDED;\n}\n\n/**\n * A source that can also be asked a topological question.\n *\n * Separate from [`BoundedSource`] rather than optional on it, because *can you walk edges* is a fact\n * about a source that a caller should learn from the type rather than from a predicate. A relation\n * with `x`/`y` and a spatial index answers regions and nothing else; one that holds adjacency — or\n * that can reach fossil's `expand` — answers both.\n *\n * `useQueryLoop` narrows with `\"explore\" in source`, which is the check a caller writes once.\n */\nexport interface ExploringSource extends BoundedSource {\n explore(request: ExploreRequest): Promise<Slice>;\n}\n\n/** Where to start and how far out. Everything else is the same bounding as a region. */\nexport interface ExploreRequest extends Omit<SliceRequest, \"view\"> {\n /** Where to start, as identities — not buffer indices, which do not survive an answer. */\n seeds: VertexId[];\n /** How many hops out. One is the ego network; beyond three is usually the whole graph. */\n depth: number;\n}\n\n/**\n * Whether this graph should be sliced at all.\n *\n * `undefined` when the source cannot say cheaply — in which case slice, because an unknown corpus is\n * more likely to be the large kind than not.\n */\nexport function shouldSlice(total: number | undefined, limit: number): boolean {\n return total === undefined || total > limit;\n}\n\n/**\n * Sensible defaults, and the reason each one is that number.\n *\n * The camera→rectangle conversion that used to live here is gone: cosmos.gl owns the screen↔space\n * transform and answers it through `screenToSpacePosition`, so deriving the rectangle from the\n * camera and the space size was a second implementation of the renderer's own maths, free to drift\n * from it. `useQueryLoop` asks the renderer instead.\n */\nexport const BOUNDED_DEFAULTS = {\n /**\n * Twenty thousand marks.\n *\n * Above about 50,000 points a live layout stops being comfortable and the edge layer is already\n * fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is\n * the legibility ceiling, which arrives first and is the one a reader actually meets.\n */\n limit: 20_000,\n /**\n * The shortest edge worth a row — three screen pixels.\n *\n * Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px\n * at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge\n * that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px\n * sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is\n * in `.planning/FAR-VIEW-AND-EDGES.md`.\n *\n * Three rather than two because both were measured against the same five windows of\n * `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of\n * image. Not on `SliceRequest`, because there is one call site and a knob with one call site is a\n * knob nobody has an opinion about — it moves when a second reader disagrees with the measurement.\n */\n minLinkPixels: 3,\n} as const;\n"],"names":["SUPERSEDED","isSuperseded","error","shouldSlice","total","limit","BOUNDED_DEFAULTS"],"mappings":"AAsSO,MAAMA,IAAa,OAAO,YAAY;AAEtC,SAASC,EAAaC,GAAyB;AACpD,SAAOA,MAAUF;AACnB;AA8BO,SAASG,EAAYC,GAA2BC,GAAwB;AAC7E,SAAOD,MAAU,UAAaA,IAAQC;AACxC;AAUO,MAAMC,IAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ9B,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeP,eAAe;AACjB;"}
|
|
1
|
+
{"version":3,"file":"bounded.js","sources":["../src/bounded.ts"],"sourcesContent":["/**\n * A graph you never hold all of.\n *\n * `load()` is the other kind: it reads a relation whole, turns it into typed arrays, and hands the\n * lot to the renderer. Measured, that costs 389 ms at 200,000 nodes and stops being viable somewhere\n * short of a million — the working set is N, so the ceiling is whatever N the machine can hold.\n *\n * This is the shape that has no such ceiling: something asks a bounded question — a rectangle, or a\n * neighbourhood a few hops wide — and the source answers with **at most `limit` points**. The\n * answer's size follows the question rather than the corpus. Moving re-asks. A window holding more\n * than `limit` is *sampled* rather than truncated, so a view of everything is still a few thousand\n * marks and they are still spread over everything — see `sampled` in `duck-source.ts`.\n *\n * **Positions are authority, not suggestion.** The corpus is written once and read many — OLAP, which\n * is what GraphAr is for — so the coordinates the batch emits are the index every spatial question\n * is asked against. That settles a tension an earlier draft of this file waved at rather than\n * resolved: re-laying-out a slice live would move points out from under the very coordinates the\n * next query is expressed in, and the camera would drift away from the index within one frame of\n * the first force. **Do not re-lay-out a slice.** If a layout is wrong, it is wrong upstream, and it\n * is fixed by recompiling — the graph is a compiler's output and so is its geometry.\n *\n * **Dragging is the exception, and it is a local overlay.** A reader can move a node; that changes\n * where it is *drawn*, never where it is *indexed*. The consequence is small and real: drag a node\n * far away, pan to where you dropped it, and the spatial query does not know it is there. Which is\n * why `pinned` exists below — the few points a reader has taken hold of ride along with every\n * slice, regardless of the rectangle.\n *\n * **Deliberately not a format.** A source is anything that can answer that question: Parquet\n * fetched by address, a plain relation with `x`/`y` columns and a spatial predicate, or an\n * in-memory index. This package renders; it does not learn a storage layout. The\n * wiring between a particular source and this contract belongs at the call site.\n *\n * **Dense indices, not database ids.** `links` refers to positions in `positions`, so a consumer\n * never pays for an id→index map — the 148 ms that mapping costs at 200,000 nodes is not optimised\n * here, it is designed away. Sources that already number their vertices densely (GraphAr's\n * `dense_id` does) hand this over for free.\n */\n\nimport type { VertexId } from \"./resident\";\n\n/**\n * What the camera is looking at, in the graph's own coordinate space.\n *\n * A rectangle and nothing else. It carried a `zoom` for one reader — the level-of-detail threshold\n * a source compared it against to decide whether to answer with super-nodes — and that branch is\n * gone, so the field went with it rather than staying as a number every caller has to invent and\n * nothing reads.\n */\nexport interface Viewport {\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/**\n * One answer. Every array is parallel and indexed densely from zero.\n *\n * `n` is what *matched*, before `limit` cut it — the difference between the two is how a view says\n * \"there is more here than I am showing you\", which is the one honest thing a bounded renderer owes\n * its reader. When a window holds more than `limit`, what comes back is a **sample** of it rather\n * than its first `limit` rows; `n` reports the window either way.\n *\n * **A struct, and it used to be a tagged union.** `mode` picked between points and super-nodes and\n * only the second branch carried `weights`. Both are gone — see `/docs/design/graph` — and with one\n * branch left a discriminant is a\n * field with one legal value.\n */\nexport interface Slice {\n n: number;\n /**\n * How many of `positions` are **marks** — points a reader can see. Everything past it is an\n * *anchor*.\n *\n * An anchor is a real vertex at its real coordinates that the window did not return: it is in the\n * buffers so that an edge leaving the window has somewhere to end. It is never drawn — `buffers`\n * gives it radius zero and alpha zero, and `residentOf` stops here, so nothing hovers, selects or\n * frames one.\n *\n * **Why the far end is the vertex rather than a point on the border.** A stub clipped to the\n * viewport carries the right direction and *lies about the distance*, and a reader cannot tell a\n * stub that ends 1.1 window-widths out from one that ends 47 — measured over all the far ends a\n * window loses, 20–22% are past four semi-widths and the worst is 47.1\n * (`.planning/FAR-VIEW-AND-EDGES.md`). Drawing the vertex where it is cannot lie, and it is also\n * the cheaper of the two: a clipped stub is one point *per edge*, an anchor is one point per far\n * *vertex*.\n *\n * Equal to `positions.length / 2` for a source that answers with marks only, which is why it is\n * required rather than optional — a caller that reads it always gets the count it meant.\n */\n marks: number;\n /**\n * Who each returned point *is*, parallel to `positions` — the `(type_idx, dense_id)` pair packed\n * by `vertexId`.\n *\n * Present because a buffer index is **not stable across answers**: index 7 is a different vertex\n * after a pan. Anything that outlives one answer — a selection, a focused node, a pinned set — has\n * to be held as an identity and re-resolved through `residentOf` each time. Leaving this out was\n * how the first draft would have shipped a selection that silently pointed at the wrong nodes.\n *\n * `BigUint64Array` because the pair is 64 bits exactly — a `Uint32Array` cannot hold it at all,\n * and a `Float64Array` holds it only while the type index stays under 2²¹, which is a ceiling\n * nobody would find until they crossed it. Still a typed array, so it is still one allocation and\n * still transferable; only what it carries changed. A dense id on its own is not an identity: it\n * numbers within one vertex type, so a union of two types repeats every value.\n */\n vertices: BigUint64Array;\n /**\n * The subject IRI of each returned point, parallel to `vertices` — **opt-in, and absent by\n * default.**\n *\n * `vertices` says where a point *is*; this says which vertex it *is*. They are not the same thing\n * and the corpus is explicit about it: redoing a layout renumbers every vertex, so a `dense_id`\n * held outside the corpus names a different vertex after the next write. Anything that has to\n * survive a recompile — a bookmark, a link out, a row in somebody else's database — keys on the\n * IRI. A selection held as `VertexId` survives a pan and does not survive a rebuild.\n *\n * **Absent by default because it costs 1.87× the tile, measured on the corpus side.** Compressed\n * bytes per row at five million: `subject` 8.016 against `dense_id` 4.000, `x` 2.717, `y` 2.501.\n * The four drawing columns are 9.23 B/row and become 17.25 with it. So the drawing path carries\n * addresses, and a host asks for names when something has to be *named* rather than painted.\n *\n * A `string[]` rather than a typed array, because that is what an IRI is. It is the one thing in a\n * `Slice` that does not go to the GPU, which is exactly why it is optional: a host that never\n * names a vertex should not pay to move it.\n */\n subjects?: string[];\n /** `[x0, y0, x1, y1, …]`, one pair per returned point — `marks` of them, then the anchors. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /** Per-point category ordinal, for colour. */\n categories: Uint16Array;\n /**\n * What the size ramp is spent on, per point — a degree, a count, whatever the corpus ranks by.\n *\n * Optional because a source may not have one, and a graph drawn at one radius is a legitimate\n * picture. But without it a look's `form.size` range has only one end, so a source that can afford\n * the column should send it: it is the difference between seeing a hub and counting one.\n */\n sizes?: Float32Array;\n}\n\n/**\n * A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a\n * reader panning across a laid-out corpus, and it is what a corpus can answer by address.\n *\n * **It is the only question this contract asks, and that is a narrowing rather than the natural\n * shape.** A network has no spatial \"near\"; it has topological near, and a rectangle cannot express\n * \"two hops from this node\" no matter how it is positioned — so a contract that only speaks\n * rectangles imposes the map metaphor on a network. `ExploringSource` said the second question here\n * and is deleted with the source that answered it; `index.test.ts` carries the tombstone and the\n * seam it named. What it comes back as is fossil's `expand`, addressed rather than joined — not a\n * second variant of this call.\n */\nexport interface SliceRequest {\n /** The rectangle. */\n view: Viewport;\n /**\n * Which column colours a point — Plot's channel name, and Plot's meaning.\n *\n * **On the request rather than on the source, and that is the whole shape.** A source says *where\n * the bytes are*; a request says *what I want to draw*, and which column colours is the second.\n * Baked into a source at construction — which is where it used to live — changing what a graph is\n * coloured by meant building a new source, and the two are not the same question.\n *\n * It is the reference's own arrangement: in Plot the **mark** carries the channels and the mark is\n * what produces the query, while the data source only says where rows come from.\n *\n * Defaults to `community`, which every corpus has because the layout pass writes it.\n */\n fill?: string;\n /**\n * Which column the size ramp is spent on — Plot's `r`.\n *\n * Omitted, every point is drawn at one radius, which is a legitimate picture: without a ramp a\n * look's `form.size` range has only one end.\n */\n r?: string;\n /**\n * Vertices that must come back whatever the query says.\n *\n * The set a reader has taken hold of — dragged, pinned, selected, focused. Their drawn positions\n * are a view-local overlay on coordinates that never move, so the index cannot find them where\n * they now appear. Carrying them explicitly is cheaper and more honest than making the index\n * mutable: it is a handful of identities, and the alternative is a spatial structure that has to be\n * rewritten every time somebody drags something.\n */\n pinned?: VertexId[];\n /**\n * The most points the source may return.\n *\n * **Above it a source samples the window; it does not take the front of it.** Which is the second\n * half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of\n * those rather than whichever ones an `ORDER BY` happened to put first.\n *\n * Required, and always filled: a host's `limit` is optional all the way down to `useQueryLoop`,\n * which resolves it before the question leaves. A source never has to know what this package\n * would have chosen.\n */\n limit: number;\n /**\n * The shortest edge worth a row, **in screen pixels** — multiply by `perPixel` for a length in the\n * graph's own space.\n *\n * **What it buys is on `perPixel` below**: two of every three edges a five-million-node window\n * draws are under one pixel long, and discarding everything under three sends a third of the rows\n * for an identical picture.\n *\n * Required for the reason `limit` is, and it is the field this whole shape exists for. It used to\n * live on an exported `BOUNDED_DEFAULTS` that a source read to find out what it was being asked —\n * so a request arrived incomplete and every source finished it in its own words. There were two,\n * one of them in another repository. Now the loop resolves it and a source reads it here.\n *\n * Meaningless without `perPixel`, and a source with no resolution discards nothing: a threshold in\n * pixels with no pixels is not a threshold.\n */\n minLinkPixels: number;\n /**\n * How much of the graph's own space one screen pixel covers — the resolution the answer is going\n * to be looked at.\n *\n * **What it buys: an edge shorter than three pixels is not sent.** Two of every three edges a\n * five-million-node window draws are under one pixel long — they are a dot on top of their own\n * endpoints, which the point layer has already drawn. Discarding everything under 3 px sends\n * 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical\n * (`.planning/FAR-VIEW-AND-EDGES.md`, measured 2026-08-17).\n *\n * **A row discard, not a fade.** cosmos.gl's `linkVisibilityDistanceRange` already dims a short\n * link, and dimming happens after the row has been joined, returned, uploaded and rasterised. This\n * is the same picture without the work.\n *\n * The rectangle alone cannot say it: the same rectangle over a 400-pixel canvas and a 4,000-pixel\n * one are different questions, and only the caller knows which. Omitted — a source is asked for\n * everything, or by something with no canvas — nothing is discarded, because a threshold in pixels\n * with no pixels is not a threshold.\n */\n perPixel?: number;\n /**\n * Stop: the caller does not want this answer any more.\n *\n * **A cancelled question rejects with `signal.reason`, and that is the whole contract.** The\n * camera moves faster than a database answers, so a source that can only hold one question at a\n * time is routinely asked a second before the first has landed. The first caller is still holding\n * a promise; dropping it leaves that caller's `finally` unrun, which in `useQueryLoop` reads as a\n * query permanently in flight for the rest of the session. So the stale promise is **settled**,\n * and this says with what.\n *\n * `AbortController.abort()` puts a `DOMException` named `AbortError` in `reason`, and `throw\n * signal.reason` is the whole implementation. A source that cancels for its own reasons — one\n * standing question re-aimed by a newer caller, a connection let go — throws an `AbortError` it\n * builds itself, which is what `abortError()` below is for.\n *\n * **This replaced a sentinel of ours, and the argument for the sentinel was real.** `SUPERSEDED`\n * was an exported `Symbol` with `isSuperseded` beside it, on the reasoning that *you moved on* and\n * *the database said no* are the two things a query loop must tell apart, and a string comparison\n * against a thrown value goes stale with nothing failing. All of that is true and none of it is an\n * argument for a *private* sentinel: `AbortError` is the name the platform already gives that\n * distinction, `fetch` rejects with it, and every third-party async primitive a source is written\n * over — `fetch`, `AbortSignal.timeout`, a WHATWG stream — produces one without being told to.\n * Ours meant a source had to import a symbol from us to be cancellable at all, and a source that\n * simply passed the signal to `fetch` did the standard thing and was reported to the host as a\n * failure. The comparison is against `error.name`, which is the platform's contract rather than a\n * message.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Anything that can answer \"what is in this rectangle, at this zoom, in at most this many marks\".\n *\n * One method, one question. A source that also wants search, aggregation or paths is describing a\n * query surface rather than a render path, and there is already one of those — fossil's verbs. The\n * line: this answers *what should I draw*, and nothing about *what does it mean*.\n */\nexport interface BoundedSource {\n slice(request: SliceRequest): Promise<Slice>;\n /**\n * How many vertices there are in total, if the source knows cheaply.\n *\n * The reason this exists is the one real cost of bounding: **a graph that fits pays for nothing.**\n * Unbounded loads once and then pans on the GPU for free; bounded issues a query per camera move.\n * At 200,000 nodes that trade is overwhelmingly worth it — 1,017 ms of first paint against roughly\n * 130 ms — but at 2,000 it is pure loss, tens of milliseconds of latency per pan buying a ceiling\n * nobody was near.\n *\n * So a consumer asks first. Under the limit, take one slice covering everything and never ask\n * again: same code path, and panning is free exactly when it can be.\n */\n total?(): Promise<number>;\n /**\n * The rectangle the corpus occupies, when the source can say cheaply.\n *\n * **Framing the opening view is not the host's job, and treating it as one is measured.** The\n * archive occupies `x ∈ [1843, 2253]` of a 4,096-wide space — 1% of its area — and a camera that\n * opens on the space rather than on the data draws 1,543 points into a tenth of the viewport, at\n * two to nine pixels each, under a fog of links. Every point is uploaded and none is legible, which\n * a reader reports as *the nodes are not rendering*.\n *\n * It matters more for a bounded source than for a whole one: the first question a sliced graph\n * asks is *what is the camera over*, so a camera pointing at empty space is a first paint of\n * nothing. Framing before asking is the difference between one query and none.\n *\n * Optional, because a source over an unlaid-out relation has no answer — and cheap where it\n * exists: a corpus reads it off tile footers it was going to read anyway, and a relation with\n * `x`/`y` gets it from four aggregates.\n */\n extent?(): Promise<Viewport>;\n /**\n * Say when the answer changes for a reason the camera cannot see, and hold the source's resources\n * for as long as anybody is listening.\n *\n * **A whole slice rather than a nudge to ask again, and that is what makes it worth having.** The\n * reason a bounded answer changes on its own is that the page filtered something — somebody\n * brushed a histogram, a panel picked a value — and a source that lives inside a crossfilter is\n * *told* by the coordinator, which has already re-run the reads with the new predicate by the time\n * this fires. Asking again would issue the same two queries a second time to learn what is in hand.\n *\n * The returned function is also the release: it is where a source lets go of whatever it holds —\n * a client registration, a connection, a cache — so a loop that calls this is a loop that cannot\n * leak one. Optional, because a source over arrays holds nothing and changes for nothing.\n */\n watch?(answered: (slice: Slice) => void): () => void;\n}\n\n/**\n * A cancellation this source is raising itself, in the platform's own shape.\n *\n * For the case a `signal` cannot express: a source holding **one** standing question, re-aimed by a\n * newer caller. There is no signal for the caller that lost — its request was never aborted, it was\n * simply overtaken — so the source builds the rejection, and it builds the same kind the platform\n * would have. `message` says what overtook it; `name` is what anybody tests.\n *\n * Not on the barrel. A source outside this package cancels by passing the `signal` it was handed to\n * whatever it is waiting on, or by `throw signal.reason` — both of which produce an `AbortError`\n * with nothing imported from us. This exists for the one case that has no signal to reach for, and\n * `new DOMException(message, \"AbortError\")` is the whole of it if a third source ever needs it too.\n */\nexport function abortError(message: string): DOMException {\n return new DOMException(message, \"AbortError\");\n}\n\n/**\n * Whether a rejection means *you moved on* rather than *the answer failed*.\n *\n * `error.name` and not `instanceof`: the same `AbortError` reaches here as a `DOMException` from\n * `AbortController`, as one of ours from `abortError`, and — for a source written over `fetch` in\n * another realm, an iframe or a worker — as an object no `instanceof` in this realm matches. The\n * name is what WHATWG specifies and what every producer agrees on.\n *\n * Not on the barrel either, for the same reason as `abortError`: a caller of this package awaits\n * `slice()` behind an `AbortController` it owns, so `controller.signal.aborted` already answers the\n * question for it. This is the branch `useQueryLoop` needs for the *other* half — a source that\n * cancelled a question the loop had not aborted.\n */\nexport function isAbort(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as { name?: unknown }).name === \"AbortError\";\n}\n\n/**\n * Whether this graph should be sliced at all.\n *\n * `undefined` when the source cannot say cheaply — in which case slice, because an unknown corpus is\n * more likely to be the large kind than not.\n */\nexport function shouldSlice(total: number | undefined, limit: number): boolean {\n return total === undefined || total > limit;\n}\n\n/**\n * What a question means when the host says nothing — **resolved before a source ever sees it.**\n *\n * These were `BOUNDED_DEFAULTS`, an exported table, and a source read it to find out what it was\n * being asked. That is the defect, stated plainly: a request that arrives unresolved is an\n * *incomplete* request, and every source outside this package had to know a constant of ours to\n * finish it. Two of them did — `duck-source.ts` here, and fossil's tile reader — and each finished\n * it in its own words, which is two chances to disagree about one number.\n *\n * `useQueryLoop` fills both in on every call now, so `limit` and `minLinkPixels` are **required**\n * fields of `SliceRequest`: a source reads `request.limit` and is done. The numbers stay module\n * scoped and off the barrel, because nobody outside needs to look them up any more.\n *\n * The camera→rectangle conversion that used to live beside them is gone for the sibling reason:\n * cosmos.gl owns the screen↔space transform and answers it through `screenToSpacePosition`, so\n * deriving the rectangle from the camera and the space size was a second implementation of the\n * renderer's own maths, free to drift from it. `useQueryLoop` asks the renderer instead.\n */\n\n/**\n * Twenty thousand marks.\n *\n * Above about 50,000 points a live layout stops being comfortable and the edge layer is already\n * fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is\n * the legibility ceiling, which arrives first and is the one a reader actually meets.\n */\nexport const DEFAULT_LIMIT = 20_000;\n\n/**\n * The shortest edge worth a row — three screen pixels.\n *\n * Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px\n * at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge\n * that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px\n * sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is\n * in `.planning/FAR-VIEW-AND-EDGES.md`.\n *\n * Three rather than two because both were measured against the same five windows of\n * `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of\n * image.\n *\n * **It is on `SliceRequest` now, and the argument against that has been overtaken.** It used to say:\n * one call site, and a knob with one call site is a knob nobody has an opinion about. There were\n * two call sites by then and one of them was in another repository, both reading the constant off\n * the barrel to reconstruct the same product. Whether it is a *knob* is still open — no caller\n * overrides it — but it is a **fact about the question**, and a question carries its own facts.\n */\nexport const DEFAULT_MIN_LINK_PIXELS = 3;\n"],"names":["abortError","message","isAbort","error","shouldSlice","total","limit","DEFAULT_LIMIT","DEFAULT_MIN_LINK_PIXELS"],"mappings":"AAkVO,SAASA,EAAWC,GAA+B;AACxD,SAAO,IAAI,aAAaA,GAAS,YAAY;AAC/C;AAeO,SAASC,EAAQC,GAAyB;AAC/C,SAAO,OAAOA,KAAU,YAAYA,MAAU,QAASA,EAA6B,SAAS;AAC/F;AAQO,SAASC,EAAYC,GAA2BC,GAAwB;AAC7E,SAAOD,MAAU,UAAaA,IAAQC;AACxC;AA4BO,MAAMC,IAAgB,KAqBhBC,IAA0B;"}
|