@modernrelay/orbit-core 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { DIAGNOSTIC_SAMPLE_CAP, encodeStringTable, collectTransfers, deriveClusters, resolveClusterCenters, clusterCentroids, EnvelopeSequencer, RequestLedger } from './chunk-KRCG2JFN.js';
2
- export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, acceptColumnar, clusterCentroids, collectTransfers, decodeStringTable, deriveClusters, encodeStringTable, generateClusterCenters, judgeEpoch, resolveClusterCenters } from './chunk-KRCG2JFN.js';
1
+ import { DIAGNOSTIC_SAMPLE_CAP, resolveSimulation, encodeStringTable, collectTransfers, deriveClusters, resolveClusterCenters, clusterCentroids, EnvelopeSequencer, RequestLedger } from './chunk-DCY66QZS.js';
2
+ export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, SIMULATION_PRESETS, acceptColumnar, clusterCentroids, collectTransfers, decodeStringTable, deriveClusters, encodeStringTable, generateClusterCenters, judgeEpoch, resolveClusterCenters, resolveSimulation } from './chunk-DCY66QZS.js';
3
3
  import { createStore } from 'zustand/vanilla';
4
4
 
5
5
  // src/errors.ts
@@ -123,6 +123,10 @@ function validateSnapshot(snapshot) {
123
123
  record(invalidEdge, typeof edge.id === "string" ? edge.id : `[${i}]`);
124
124
  continue;
125
125
  }
126
+ if (typeof edge.id === "string" && edge.id.includes("\0")) {
127
+ record(invalidEdge, `[${i}]`);
128
+ continue;
129
+ }
126
130
  if (!nodeIndex.has(source)) {
127
131
  record(danglingEdge, source);
128
132
  continue;
@@ -172,7 +176,7 @@ function validateSnapshot(snapshot) {
172
176
  "invalid-edge",
173
177
  "error",
174
178
  invalidEdge,
175
- `${invalidEdge.count} edge row(s) dropped: missing or non-string source/target`
179
+ `${invalidEdge.count} edge row(s) dropped: missing or non-string source/target, or NUL-containing explicit id`
176
180
  );
177
181
  pushDiagnostic(
178
182
  diagnostics,
@@ -1239,7 +1243,7 @@ function stageBatch(contribution, batch, tallies, nextOrder) {
1239
1243
  for (let i = 0; i < rawNodes.length; i++) {
1240
1244
  const row = rawNodes[i];
1241
1245
  const id = typeof row === "object" && row !== null ? row.id : void 0;
1242
- if (typeof id !== "string") {
1246
+ if (typeof id !== "string" || id.includes("\0")) {
1243
1247
  recordRow(tallies.invalidNodes, `[${batch.sequence}:${i}]`);
1244
1248
  continue;
1245
1249
  }
@@ -1263,6 +1267,10 @@ function stageBatch(contribution, batch, tallies, nextOrder) {
1263
1267
  recordRow(tallies.invalidEdges, typeof edge.id === "string" ? edge.id : `[${batch.sequence}:${i}]`);
1264
1268
  continue;
1265
1269
  }
1270
+ if (typeof edge.id === "string" && edge.id.includes("\0")) {
1271
+ recordRow(tallies.invalidEdges, `[${batch.sequence}:${i}]`);
1272
+ continue;
1273
+ }
1266
1274
  const hasExplicitId = typeof edge.id === "string";
1267
1275
  let id;
1268
1276
  if (hasExplicitId) {
@@ -1415,7 +1423,7 @@ function sessionCommitDiagnostics(tallies, danglingCount, danglingSamples = [])
1415
1423
  "invalid-node",
1416
1424
  "error",
1417
1425
  tallies.invalidNodes,
1418
- `${tallies.invalidNodes.count} ingested node row(s) dropped: missing or non-string id`
1426
+ `${tallies.invalidNodes.count} ingested node row(s) dropped: missing, non-string, or NUL-containing id`
1419
1427
  );
1420
1428
  push(
1421
1429
  "duplicate-node-id",
@@ -1427,7 +1435,7 @@ function sessionCommitDiagnostics(tallies, danglingCount, danglingSamples = [])
1427
1435
  "invalid-edge",
1428
1436
  "error",
1429
1437
  tallies.invalidEdges,
1430
- `${tallies.invalidEdges.count} ingested edge row(s) dropped: missing or non-string source/target`
1438
+ `${tallies.invalidEdges.count} ingested edge row(s) dropped: missing or non-string source/target, or NUL-containing explicit id`
1431
1439
  );
1432
1440
  push(
1433
1441
  "duplicate-edge-id",
@@ -2982,6 +2990,16 @@ function checkStringColumn(col, rows, where, isIds, out) {
2982
2990
  out.push({ where, problem: "not-a-string-column", detail: 'expected {kind:"string", dictionary, codes}' });
2983
2991
  return;
2984
2992
  }
2993
+ for (let d = 0; d < col.dictionary.length; d++) {
2994
+ if (typeof col.dictionary[d] !== "string") {
2995
+ out.push({
2996
+ where,
2997
+ problem: "not-a-string-column",
2998
+ detail: `dictionary entry ${d} is not a string`
2999
+ });
3000
+ return;
3001
+ }
3002
+ }
2985
3003
  if (col.codes.length !== rows) {
2986
3004
  const detached = col.codes.length === 0 && col.codes.buffer.byteLength === 0;
2987
3005
  out.push({
@@ -3230,10 +3248,12 @@ function detachColumnarBuffers(snapshot) {
3230
3248
  return detached;
3231
3249
  }
3232
3250
 
3233
- // src/worker/lane.ts
3234
- function defaultWorkerUrl() {
3235
- return new URL("./worker/entry.js", import.meta.url);
3251
+ // src/workerAsset.ts
3252
+ function createDefaultWorker() {
3253
+ return new Worker(new URL("./worker/entry.js", import.meta.url), { type: "module" });
3236
3254
  }
3255
+
3256
+ // src/worker/lane.ts
3237
3257
  function transportFromWorker(worker) {
3238
3258
  return {
3239
3259
  post: (envelope, transfers) => worker.postMessage(envelope, [...transfers]),
@@ -3275,10 +3295,7 @@ var WorkerLane = class {
3275
3295
  let transport = this.options.transport ?? null;
3276
3296
  if (transport === null) {
3277
3297
  const factory = this.options.factory;
3278
- const worker = factory !== void 0 && "create" in factory ? factory.create() : new Worker(
3279
- factory !== void 0 && "url" in factory ? factory.url : defaultWorkerUrl(),
3280
- { type: "module" }
3281
- );
3298
+ const worker = factory !== void 0 && "create" in factory ? factory.create() : factory !== void 0 && "url" in factory ? new Worker(factory.url, { type: "module" }) : createDefaultWorker();
3282
3299
  transport = transportFromWorker(worker);
3283
3300
  }
3284
3301
  transport.onReply((reply) => this.settle(reply));
@@ -5026,6 +5043,7 @@ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
5026
5043
  var isBool = (v) => typeof v === "boolean";
5027
5044
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5028
5045
  var isStrArray = (v) => Array.isArray(v) && v.every(isStr);
5046
+ var isNumArray = (v) => Array.isArray(v) && v.every(isNum);
5029
5047
  function validateBrush(v, at, problems) {
5030
5048
  if (!isObj(v)) {
5031
5049
  problems.push(`${at}: not an object`);
@@ -5044,18 +5062,43 @@ function validateBrush(v, at, problems) {
5044
5062
  }
5045
5063
  problems.push(`${at}: unknown brush kind`);
5046
5064
  }
5047
- function validateScale(v, at, problems) {
5065
+ function validateScale(v, at, valueKind, problems) {
5048
5066
  if (!isObj(v)) {
5049
5067
  problems.push(`${at}: not an object`);
5050
5068
  return;
5051
5069
  }
5052
- if (v["kind"] === "sequential" || v["kind"] === "diverging") {
5070
+ const isValueArray = valueKind === "string" ? isStrArray : isNumArray;
5071
+ const valueLabel = valueKind === "string" ? "string" : "finite number";
5072
+ if (v["kind"] === "sequential") {
5053
5073
  if (!isStr(v["metric"])) problems.push(`${at}: metric must be a string`);
5054
- if (!Array.isArray(v["range"])) problems.push(`${at}: range must be an array`);
5074
+ if (!isValueArray(v["range"]) || v["range"].length !== 2) {
5075
+ problems.push(`${at}: sequential range must be a [${valueLabel}, ${valueLabel}] tuple`);
5076
+ }
5077
+ if (v["domain"] !== void 0 && (!isNumArray(v["domain"]) || v["domain"].length !== 2)) {
5078
+ problems.push(`${at}: sequential domain must be a [min, max] finite-number tuple`);
5079
+ }
5080
+ return;
5081
+ }
5082
+ if (v["kind"] === "diverging") {
5083
+ if (!isStr(v["metric"])) problems.push(`${at}: metric must be a string`);
5084
+ if (!isValueArray(v["range"]) || v["range"].length !== 3) {
5085
+ problems.push(
5086
+ `${at}: diverging range must be a [${valueLabel}, ${valueLabel}, ${valueLabel}] tuple`
5087
+ );
5088
+ }
5089
+ if (!isNum(v["mid"])) {
5090
+ problems.push(`${at}: diverging mid must be a finite number`);
5091
+ }
5055
5092
  return;
5056
5093
  }
5057
5094
  if (v["kind"] === "categorical") {
5058
5095
  if (!isStr(v["by"])) problems.push(`${at}: categorical 'by' must be a field string`);
5096
+ if (v["domain"] !== void 0 && !isStrArray(v["domain"])) {
5097
+ problems.push(`${at}: categorical domain must be a string[]`);
5098
+ }
5099
+ if (v["palette"] !== void 0 && !isValueArray(v["palette"])) {
5100
+ problems.push(`${at}: categorical palette must be a ${valueLabel}[]`);
5101
+ }
5059
5102
  return;
5060
5103
  }
5061
5104
  problems.push(`${at}: unknown scale kind`);
@@ -5180,10 +5223,10 @@ function validateViewState(raw) {
5180
5223
  problems.push("styling: must be an object");
5181
5224
  } else {
5182
5225
  if (styling["nodeColor"] !== void 0) {
5183
- validateScale(styling["nodeColor"], "styling.nodeColor", problems);
5226
+ validateScale(styling["nodeColor"], "styling.nodeColor", "string", problems);
5184
5227
  }
5185
5228
  if (styling["nodeSize"] !== void 0) {
5186
- validateScale(styling["nodeSize"], "styling.nodeSize", problems);
5229
+ validateScale(styling["nodeSize"], "styling.nodeSize", "number", problems);
5187
5230
  }
5188
5231
  for (const flag of ["showLinks", "edgeArrows"]) {
5189
5232
  if (styling[flag] !== void 0 && !isBool(styling[flag])) {
@@ -6339,6 +6382,9 @@ function pruneIds(ids, base) {
6339
6382
  function createGraphInstance(opts) {
6340
6383
  const engineFactory = opts.engine;
6341
6384
  const fitViewOnFirstData = opts.fitViewOnFirstData ?? true;
6385
+ const fitViewOnSettle = opts.fitViewOnSettle ?? "follow";
6386
+ const fitViewMaxZoom = opts.fitViewMaxZoom === void 0 ? 1.5 : opts.fitViewMaxZoom;
6387
+ const clampFit = (o) => fitViewMaxZoom === null ? o : { ...o, maxZoom: fitViewMaxZoom };
6342
6388
  const store = createStore(() => ({
6343
6389
  status: "idle",
6344
6390
  revisions: { source: null, model: 0, scope: 0, render: 0, appliedRender: null },
@@ -6507,7 +6553,7 @@ function createGraphInstance(opts) {
6507
6553
  let linkColor;
6508
6554
  let linkWidth;
6509
6555
  let layout = "force";
6510
- let simulation;
6556
+ let simulation = resolveSimulation(void 0);
6511
6557
  let theme = resolveTheme(void 0);
6512
6558
  let edgeArrows = false;
6513
6559
  let showLinks = true;
@@ -8193,7 +8239,7 @@ function createGraphInstance(opts) {
8193
8239
  simRestarted = true;
8194
8240
  }
8195
8241
  }
8196
- eng.commit(commit);
8242
+ commitToEngine(eng, commit);
8197
8243
  revisions.appliedRender = eng.appliedRevision();
8198
8244
  const facade = session !== null ? session.edgePicking : null;
8199
8245
  if (facade !== null) {
@@ -9078,12 +9124,41 @@ function createGraphInstance(opts) {
9078
9124
  if (idx !== void 0) applyEmphasis(eng, idx);
9079
9125
  }
9080
9126
  }
9127
+ const SETTLE_FOLLOW_INTERVAL_FRAMES = 55;
9128
+ const SETTLE_FOLLOW_CAP_FRAMES = 480;
9129
+ let settleFollowFrames = null;
9130
+ let settleFollowContainer = null;
9131
+ const settleFollowCancelListener = () => {
9132
+ cancelSettleFollow();
9133
+ };
9134
+ function cancelSettleFollow() {
9135
+ if (settleFollowContainer !== null) {
9136
+ settleFollowContainer.removeEventListener?.("pointerdown", settleFollowCancelListener);
9137
+ settleFollowContainer.removeEventListener?.("wheel", settleFollowCancelListener);
9138
+ settleFollowContainer = null;
9139
+ }
9140
+ settleFollowFrames = null;
9141
+ }
9142
+ function armSettleFollow(s) {
9143
+ if (fitViewOnSettle === false || layout !== "force") return;
9144
+ settleFollowFrames = 0;
9145
+ if (typeof s.container.addEventListener === "function") {
9146
+ settleFollowContainer = s.container;
9147
+ s.container.addEventListener("pointerdown", settleFollowCancelListener);
9148
+ s.container.addEventListener("wheel", settleFollowCancelListener);
9149
+ }
9150
+ }
9151
+ function settleFollowFit(eng, durationMs) {
9152
+ if (effectiveReducedMotion()) eng.fitView(clampFit({ durationMs: 0 }));
9153
+ else eng.fitView(clampFit({ durationMs }));
9154
+ }
9081
9155
  function maybeFitView(eng) {
9082
9156
  if (!fitViewOnFirstData || session === null || session.fitDone) return;
9083
9157
  if (accepted === null) return;
9084
9158
  session.fitDone = true;
9085
- if (effectiveReducedMotion()) eng.fitView({ durationMs: 0 });
9086
- else eng.fitView();
9159
+ if (effectiveReducedMotion()) eng.fitView(clampFit({ durationMs: 0 }));
9160
+ else eng.fitView(clampFit({}));
9161
+ armSettleFollow(session);
9087
9162
  }
9088
9163
  function nodeIndexBase() {
9089
9164
  return accepted === null ? EMPTY_INDEX2 : accepted.nodeIndex;
@@ -11498,10 +11573,13 @@ function createGraphInstance(opts) {
11498
11573
  {
11499
11574
  const c = {};
11500
11575
  let any = false;
11501
- if (update.simulation !== void 0 && !Object.is(update.simulation, simulation)) {
11502
- simulation = update.simulation;
11503
- c.simulation = update.simulation;
11504
- any = true;
11576
+ if (update.simulation !== void 0) {
11577
+ const nextSimulation = resolveSimulation(update.simulation);
11578
+ if (!Object.is(nextSimulation, simulation)) {
11579
+ simulation = nextSimulation;
11580
+ c.simulation = nextSimulation;
11581
+ any = true;
11582
+ }
11505
11583
  }
11506
11584
  if (update.theme !== void 0) {
11507
11585
  const nextTheme = resolveTheme(update.theme);
@@ -12082,6 +12160,14 @@ function createGraphInstance(opts) {
12082
12160
  onFrame(timeMs) {
12083
12161
  if (!active()) return;
12084
12162
  frameCadence += 1;
12163
+ if (settleFollowFrames !== null && fitViewOnSettle === "follow") {
12164
+ settleFollowFrames += 1;
12165
+ if (settleFollowFrames > SETTLE_FOLLOW_CAP_FRAMES) {
12166
+ cancelSettleFollow();
12167
+ } else if (settleFollowFrames % SETTLE_FOLLOW_INTERVAL_FRAMES === 0) {
12168
+ settleFollowFit(s.engine, 650);
12169
+ }
12170
+ }
12085
12171
  pressureSampler.noteFrame(timeMs, !store.getState().simulationRunning);
12086
12172
  flushCrossfilterNotify();
12087
12173
  if (timeMs - lastPerfSampleAt >= PERF_SAMPLE_THROTTLE_MS) {
@@ -12128,6 +12214,10 @@ function createGraphInstance(opts) {
12128
12214
  },
12129
12215
  onSimulationEnd() {
12130
12216
  if (!active()) return;
12217
+ if (settleFollowFrames !== null) {
12218
+ settleFollowFit(s.engine, 800);
12219
+ cancelSettleFollow();
12220
+ }
12131
12221
  if (accretionPinIds !== null) {
12132
12222
  accretionPinIds = null;
12133
12223
  pushPinsToEngine(s.engine, store.getState().pins);
@@ -12361,6 +12451,7 @@ function createGraphInstance(opts) {
12361
12451
  s.alive = false;
12362
12452
  session = null;
12363
12453
  mountPromise = null;
12454
+ cancelSettleFollow();
12364
12455
  cancelRerankTimer();
12365
12456
  flushCrossfilterNotify();
12366
12457
  if (quiescenceTimer !== null) {
@@ -12489,33 +12580,34 @@ function createGraphInstance(opts) {
12489
12580
  return void 0;
12490
12581
  }
12491
12582
  if (!isScaleValue(value)) return void 0;
12492
- if (value.kind === "sequential") {
12583
+ const scale = value;
12584
+ if (scale.kind === "sequential") {
12493
12585
  return {
12494
12586
  kind: "sequential",
12495
- metric: value.metric,
12496
- range: value.range,
12587
+ metric: scale.metric,
12588
+ range: scale.range,
12497
12589
  // The numeric-array domain form is data; a DomainPolicy is app
12498
12590
  // behavior config and stays with the app.
12499
- ...Array.isArray(value.domain) ? { domain: value.domain } : {}
12591
+ ...Array.isArray(scale.domain) ? { domain: scale.domain } : {}
12500
12592
  };
12501
12593
  }
12502
- if (value.kind === "diverging") {
12594
+ if (scale.kind === "diverging") {
12503
12595
  return {
12504
12596
  kind: "diverging",
12505
- metric: value.metric,
12506
- range: value.range,
12507
- mid: value.mid
12597
+ metric: scale.metric,
12598
+ range: scale.range,
12599
+ mid: scale.mid
12508
12600
  };
12509
12601
  }
12510
- if (typeof value.by !== "string") {
12602
+ if (typeof scale.by !== "string") {
12511
12603
  warnViewStateDrop(channel, "a function-`by` categorical scale does not serialize");
12512
12604
  return void 0;
12513
12605
  }
12514
12606
  return {
12515
12607
  kind: "categorical",
12516
- by: value.by,
12517
- ...value.palette !== void 0 ? { palette: value.palette } : {},
12518
- ...value.domain !== void 0 ? { domain: value.domain } : {}
12608
+ by: scale.by,
12609
+ ...scale.palette !== void 0 ? { palette: scale.palette } : {},
12610
+ ...scale.domain !== void 0 ? { domain: scale.domain } : {}
12519
12611
  };
12520
12612
  }
12521
12613
  function serializableThemeOf() {
@@ -12835,7 +12927,7 @@ function createGraphInstance(opts) {
12835
12927
  ...prevState.revisions,
12836
12928
  render: prevState.revisions.render + 1
12837
12929
  };
12838
- eng.commit({
12930
+ commitToEngine(eng, {
12839
12931
  revision: nextRevisions.render,
12840
12932
  structure: {
12841
12933
  pointCount: scene.count,
@@ -12843,6 +12935,7 @@ function createGraphInstance(opts) {
12843
12935
  links: scene.links
12844
12936
  }
12845
12937
  });
12938
+ nextRevisions.appliedRender = eng.appliedRevision();
12846
12939
  eng.pause();
12847
12940
  const patch = { revisions: nextRevisions };
12848
12941
  if (prevState.simulationRunning) patch.simulationRunning = false;
@@ -12851,6 +12944,7 @@ function createGraphInstance(opts) {
12851
12944
  }
12852
12945
  }
12853
12946
  if (state.camera !== null) {
12947
+ cancelSettleFollow();
12854
12948
  const eng = engineIfReady();
12855
12949
  if (eng !== null) {
12856
12950
  if (effectiveReducedMotion()) eng.setViewport(state.camera, { durationMs: 0 });
@@ -13170,6 +13264,7 @@ function createGraphInstance(opts) {
13170
13264
  });
13171
13265
  }
13172
13266
  function focusNode(id, opts2) {
13267
+ cancelSettleFollow();
13173
13268
  const eng = engineIfReady();
13174
13269
  if (eng === null || scene === null) return EMPTY_IDS;
13175
13270
  const idx = scene.indexById.get(id);
@@ -13250,6 +13345,7 @@ function createGraphInstance(opts) {
13250
13345
  return bytes;
13251
13346
  }
13252
13347
  function cameraZoom(factor) {
13348
+ cancelSettleFollow();
13253
13349
  const eng = engineIfReady();
13254
13350
  if (eng === null) return;
13255
13351
  if (effectiveReducedMotion()) eng.zoom(factor, 0);
@@ -13303,10 +13399,11 @@ function createGraphInstance(opts) {
13303
13399
  destroy,
13304
13400
  on,
13305
13401
  fitView: () => {
13402
+ cancelSettleFollow();
13306
13403
  const eng = engineIfReady();
13307
13404
  if (eng === null) return;
13308
- if (effectiveReducedMotion()) eng.fitView({ durationMs: 0 });
13309
- else eng.fitView();
13405
+ if (effectiveReducedMotion()) eng.fitView(clampFit({ durationMs: 0 }));
13406
+ else eng.fitView(clampFit({}));
13310
13407
  },
13311
13408
  zoomIn: () => {
13312
13409
  cameraZoom(ZOOM_STEP);
@@ -13315,6 +13412,7 @@ function createGraphInstance(opts) {
13315
13412
  cameraZoom(1 / ZOOM_STEP);
13316
13413
  },
13317
13414
  setViewport: (v) => {
13415
+ cancelSettleFollow();
13318
13416
  const eng = engineIfReady();
13319
13417
  if (eng === null) return;
13320
13418
  if (effectiveReducedMotion()) eng.setViewport(v, { durationMs: 0 });