@bicharts/chart-host 0.6.81 → 0.6.83
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 +73 -0
- package/dist/card.mjs +2 -2
- package/dist/{chunk-ZRSEKV4I.mjs → chunk-2XWE6YFO.mjs} +10 -10
- package/dist/{chunk-7HPVGRQ2.mjs → chunk-4KCSAHL5.mjs} +1 -1
- package/dist/{chunk-MP5S6MVW.mjs → chunk-CJNONH6V.mjs} +1 -1
- package/dist/index.mjs +14 -5
- package/dist/react.mjs +2 -2
- package/dist/testing.mjs +177 -0
- package/dist/types/host/services.d.ts +70 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/pendingGenerate.d.ts +18 -2
- package/dist/types/testing/index.d.ts +40 -1
- package/dist/types/viewState.d.ts +125 -0
- package/dist/view-state.mjs +171 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -278,6 +278,79 @@ made for: every word shares a comment with many others, so the default lit most
|
|
|
278
278
|
click. The cross-filter sent out is the same under both rules. `onSelectionPaint` reports how each
|
|
279
279
|
paint of a declaring chart decided, including when no mark matched and the default was used.
|
|
280
280
|
|
|
281
|
+
## Remembering what the reader did
|
|
282
|
+
|
|
283
|
+
A chart keeps its resting state (a sort, an expanded row, a 3D camera angle) in a small bag. It
|
|
284
|
+
reads the bag from `options.uiState` and writes it through `options.setUiState`, and generated code
|
|
285
|
+
never learns where the bag goes. Where it goes is your host's choice, and so is which keys may
|
|
286
|
+
outlive what: a redraw, a close and reopen, or a new version of the chart.
|
|
287
|
+
|
|
288
|
+
`createChartHost` still takes one `viewState` provider, as before. When your host needs more than
|
|
289
|
+
one store, describe it with a `ViewStateService`:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { sessionViewStateProvider, type ViewStateService } from "@bicharts/chart-host";
|
|
293
|
+
|
|
294
|
+
const viewState: ViewStateService = {
|
|
295
|
+
viewing: sessionViewStateProvider(el), // survives a redraw; a page switch starts clean
|
|
296
|
+
durable: workbookStore, // optional: survives close and reopen
|
|
297
|
+
policy: { // the keys your host scopes, by name
|
|
298
|
+
camera: { lifetime: "durable", dropOnNewVersion: true, dropOnFreshViewing: false, durableWhen: "rememberView" },
|
|
299
|
+
userStopped: { lifetime: "durable", dropOnNewVersion: false, dropOnFreshViewing: true },
|
|
300
|
+
},
|
|
301
|
+
versionKey: "cameraVersion", // optional: how a version change is noticed
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**What you provide.** `viewing` and `policy` are required (`policy: {}` is fine). Everything else is
|
|
306
|
+
optional, and leaving it out says something about your host:
|
|
307
|
+
|
|
308
|
+
- no `durable` store: the host can't remember across a close (a static preview, a server-side
|
|
309
|
+
render). `viewStateIsDurable(service)` is the one definition of "remembers the view", so it can't
|
|
310
|
+
be claimed without a store to back it. `noopViewStateService()` says "nothing is stored" out loud.
|
|
311
|
+
- no `chartKeys`: keys your policy doesn't name belong to the chart. They're kept, durably, through a
|
|
312
|
+
new version and a fresh viewing, and the chart validates what it reads. Set `chartKeys` to give
|
|
313
|
+
them another rule. A host that forgets everything when its chart changes sets `dropOnNewVersion`.
|
|
314
|
+
- no `versionKey`: you apply the version rule yourself, where you learn the chart changed.
|
|
315
|
+
- no `durableMaxChars`: the durable store accepts any size.
|
|
316
|
+
|
|
317
|
+
**What a key can be.** `lifetime: "viewing"` keeps a key out of the durable store; `"durable"` puts it
|
|
318
|
+
in both. `dropOnNewVersion` drops it when the chart on screen is another version (a camera is aimed
|
|
319
|
+
at one chart's axes). `dropOnFreshViewing` drops it when the bag is read at the start of a viewing
|
|
320
|
+
instead of in the one that wrote it. `durableWhen` names a preference that must be on for the key to
|
|
321
|
+
reach the durable store; the viewing store keeps it either way.
|
|
322
|
+
|
|
323
|
+
**The rules are plain functions** in `@bicharts/chart-host/view-state`, with no DOM, timers or
|
|
324
|
+
storage of their own:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { resolveViewState, commitViewState } from "@bicharts/chart-host/view-state";
|
|
328
|
+
|
|
329
|
+
const { bag, dropped } = resolveViewState(viewState, { codeVersion, freshViewing }); // before a render
|
|
330
|
+
commitViewState(next, viewState, prefs); // when the chart writes
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`resolveViewState` builds the bag the chart is handed: the viewing store, else the durable one, with
|
|
334
|
+
the policy applied. `commitViewState` decides what a write reaches: the viewing store gets all of
|
|
335
|
+
it, the durable store gets what the policy allows. Both report what they did and neither throws.
|
|
336
|
+
`viewStateAfterSwap(service, bag)` applies the version rule for a host that sees the swap itself.
|
|
337
|
+
|
|
338
|
+
**Check your stores** with the conformance suite in the testing subpath. It confirms `load` never
|
|
339
|
+
throws and reads `{}` when empty, that `save` replaces, that nested values round-trip, that the
|
|
340
|
+
viewing store survives your host being re-created on the same element, and that the durable store
|
|
341
|
+
holds what it's given across a reopen:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { assertViewStateConformance } from "@bicharts/chart-host/testing";
|
|
345
|
+
|
|
346
|
+
const report = await assertViewStateConformance(service, {
|
|
347
|
+
recreate: () => buildService(sameElement), // the host drawing again
|
|
348
|
+
reopen: () => buildService(newElement), // a new viewing over the same saved file
|
|
349
|
+
settle: () => flushPendingWrites(), // only if your durable save is debounced
|
|
350
|
+
});
|
|
351
|
+
expect(report.skipped).toEqual([]); // nothing went unchecked
|
|
352
|
+
```
|
|
353
|
+
|
|
281
354
|
## Architecture — three layers, and why it matters if you contribute
|
|
282
355
|
|
|
283
356
|
Read this before adding code. The package has **three** concerns, and they are independent.
|
package/dist/card.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
|
|
2
2
|
import {
|
|
3
3
|
computeSelectionCard
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-4KCSAHL5.mjs";
|
|
5
|
+
import "./chunk-2XWE6YFO.mjs";
|
|
6
6
|
import "./chunk-GHODWOAL.mjs";
|
|
7
7
|
|
|
8
8
|
// src/selectionCardChrome.ts
|
|
@@ -1140,7 +1140,7 @@ function F3(e) {
|
|
|
1140
1140
|
function N2(e) {
|
|
1141
1141
|
let t = F3(e);
|
|
1142
1142
|
if (t.length === 0 || ne2(e) < 95) return false;
|
|
1143
|
-
let n = t.filter((
|
|
1143
|
+
let n = t.filter((u4) => te(u4) && !t3(u4)).length, o2 = t.filter((u4) => !te(u4) && !!t3(u4)).length;
|
|
1144
1144
|
return n >= 1 && n >= o2;
|
|
1145
1145
|
}
|
|
1146
1146
|
function ce(e, t) {
|
|
@@ -1185,7 +1185,7 @@ function Yn2(e, t) {
|
|
|
1185
1185
|
let n = F3(e);
|
|
1186
1186
|
if (n.length === 0) return 0;
|
|
1187
1187
|
let o2 = 0;
|
|
1188
|
-
for (let
|
|
1188
|
+
for (let u4 of n) j3(u4, t) && o2++;
|
|
1189
1189
|
return Math.round(o2 / n.length * 1e3) / 10;
|
|
1190
1190
|
}
|
|
1191
1191
|
function ee(e, t) {
|
|
@@ -1198,13 +1198,13 @@ function Qn(e, t) {
|
|
|
1198
1198
|
for (let o2 of oe(e)) t(o2) && n++;
|
|
1199
1199
|
return n;
|
|
1200
1200
|
}
|
|
1201
|
-
function Xn(e, t, n, o2,
|
|
1202
|
-
let
|
|
1201
|
+
function Xn(e, t, n, o2, u4) {
|
|
1202
|
+
let l2 = [], a3 = [], i = {}, h4 = {}, C = new Set(e), y2 = (s2) => t.map((d2) => d2[s2]);
|
|
1203
1203
|
for (let s2 of ["city", "country"]) {
|
|
1204
1204
|
let d2 = n?.[s2];
|
|
1205
1205
|
d2 && C.has(d2) && (i[s2] = d2);
|
|
1206
1206
|
}
|
|
1207
|
-
let g5 = new Set(
|
|
1207
|
+
let g5 = new Set(u4 ?? e);
|
|
1208
1208
|
for (let s2 of ["lat", "lon"]) {
|
|
1209
1209
|
let d2 = n?.[s2];
|
|
1210
1210
|
d2 && g5.has(d2) && (i[s2] = d2);
|
|
@@ -1230,13 +1230,13 @@ function Xn(e, t, n, o2, l2) {
|
|
|
1230
1230
|
let K3 = ee(S2, (G4) => !!t3(G4)), Ne2 = ee(S2, (G4) => !!t3(G4) && !ce(G4, o2));
|
|
1231
1231
|
K3 < Z2 && Ne2 < 1 || (!s2 || O4 > s2.pct) && (s2 = { name: d2, pct: O4 });
|
|
1232
1232
|
}
|
|
1233
|
-
s2 && (i.state = s2.name, p4.add(s2.name),
|
|
1233
|
+
s2 && (i.state = s2.name, p4.add(s2.name), l2.push(`state=${s2.name} (${s2.pct}% states/provinces)`));
|
|
1234
1234
|
}
|
|
1235
1235
|
if (!i.zip) for (let s2 of b4) {
|
|
1236
1236
|
if (p4.has(s2)) continue;
|
|
1237
1237
|
let d2 = y2(s2), S2 = re2(d2);
|
|
1238
1238
|
if (!(S2 < 95) && de(d2, s2) && !(Qn(d2, (O4) => r3(O4).length > 0) < Z2)) {
|
|
1239
|
-
i.zip = s2, p4.add(s2),
|
|
1239
|
+
i.zip = s2, p4.add(s2), l2.push(`zip=${s2} (${S2}% ZIP codes)`);
|
|
1240
1240
|
break;
|
|
1241
1241
|
}
|
|
1242
1242
|
}
|
|
@@ -1248,7 +1248,7 @@ function Xn(e, t, n, o2, l2) {
|
|
|
1248
1248
|
let S2 = ne2(y2(d2));
|
|
1249
1249
|
(!s2 || S2 > s2.pct) && (s2 = { name: d2, pct: S2 });
|
|
1250
1250
|
}
|
|
1251
|
-
s2 && (T2 && (a3.push(`country=${T2} (only ${ne2(y2(T2))}% of values are country identifiers; using ${s2.name} instead)`), h4.country = T2), i.country = s2.name, p4.add(s2.name),
|
|
1251
|
+
s2 && (T2 && (a3.push(`country=${T2} (only ${ne2(y2(T2))}% of values are country identifiers; using ${s2.name} instead)`), h4.country = T2), i.country = s2.name, p4.add(s2.name), l2.push(`country=${s2.name}`));
|
|
1252
1252
|
}
|
|
1253
1253
|
if (!i.city) {
|
|
1254
1254
|
let s2 = null;
|
|
@@ -1257,13 +1257,13 @@ function Xn(e, t, n, o2, l2) {
|
|
|
1257
1257
|
let S2 = y2(d2), O4 = Yn2(S2, o2);
|
|
1258
1258
|
O4 < D3 || ee(S2, (K3) => j3(K3, o2)) < Z2 || (!s2 || O4 > s2.pct) && (s2 = { name: d2, pct: O4 });
|
|
1259
1259
|
}
|
|
1260
|
-
s2 && (i.city = s2.name,
|
|
1260
|
+
s2 && (i.city = s2.name, l2.push(`city=${s2.name} (${s2.pct}% known cities)`));
|
|
1261
1261
|
}
|
|
1262
1262
|
let L4 = i.country ? y2(i.country) : [], R4 = !!i.country && N2(L4), A = R4 ? new Set(L4.map((s2) => k(s2 == null ? null : String(s2))).filter(Boolean)).size : 0, x3 = R4 && A >= 2, w4 = !!(i.lat && i.lon) && t.some((s2) => {
|
|
1263
1263
|
let d2 = +String(s2[i.lat] ?? "").trim(), S2 = +String(s2[i.lon] ?? "").trim();
|
|
1264
1264
|
return isFinite(d2) && isFinite(S2) && d2 >= -90 && d2 <= 90 && S2 >= -180 && S2 <= 180 && String(s2[i.lat] ?? "").trim() !== "" && String(s2[i.lon] ?? "").trim() !== "";
|
|
1265
1265
|
});
|
|
1266
|
-
return i.lat && i.lon && !w4 && a3.push(`lat/lon=${i.lat}/${i.lon} (no row holds a usable coordinate pair)`), i.country && !x3 && !(i.city || i.state || i.zip || w4) && a3.push(R4 ? `country=${i.country} (every row resolves to the same country - nothing to place apart)` : `country=${i.country} (values are not country identifiers)`), { bind: !!(i.city || i.state || i.zip || x3 || w4) ? i : null, backfilled:
|
|
1266
|
+
return i.lat && i.lon && !w4 && a3.push(`lat/lon=${i.lat}/${i.lon} (no row holds a usable coordinate pair)`), i.country && !x3 && !(i.city || i.state || i.zip || w4) && a3.push(R4 ? `country=${i.country} (every row resolves to the same country - nothing to place apart)` : `country=${i.country} (values are not country identifiers)`), { bind: !!(i.city || i.state || i.zip || x3 || w4) ? i : null, backfilled: l2, refused: a3, ...Object.keys(h4).length ? { labelFallback: h4 } : {} };
|
|
1267
1267
|
}
|
|
1268
1268
|
var r = (e, ...t) => ({ part: e, langs: Object.freeze(t) });
|
|
1269
1269
|
var ie = Object.freeze({ jaar: r("year", "nl"), kwartaal: r("quarter", "nl"), maand: r("month", "nl"), dag: r("day", "nl", "sv", "da", "nb"), jahr: r("year", "de"), quartal: r("quarter", "de"), monat: r("month", "de"), tag: r("day", "de"), ann\u00E9e: r("year", "fr"), mois: r("month", "fr"), jour: r("day", "fr"), trimestre: r("quarter", "fr", "es", "pt", "it"), a\u00F1o: r("year", "es"), mes: r("month", "es"), d\u00EDa: r("day", "es"), ano: r("year", "pt"), m\u00EAs: r("month", "pt"), dia: r("day", "pt"), anno: r("year", "it"), mese: r("month", "it"), giorno: r("day", "it"), \u0433\u043E\u0434: r("year", "ru"), \u043C\u0435\u0441\u044F\u0446: r("month", "ru"), \u0440\u0456\u043A: r("year", "uk"), \u043C\u0456\u0441\u044F\u0446\u044C: r("month", "uk"), \u043A\u0432\u0430\u0440\u0442\u0430\u043B: r("quarter", "ru", "uk"), \u0434\u0435\u043D\u044C: r("day", "ru", "uk"), rok: r("year", "pl", "cs", "sk"), kwarta\u0142: r("quarter", "pl"), miesi\u0105c: r("month", "pl"), dzie\u0144: r("day", "pl"), \u010Dtvrtlet\u00ED: r("quarter", "cs"), m\u011Bs\u00EDc: r("month", "cs"), den: r("day", "cs"), \u0161tvr\u0165rok: r("quarter", "sk"), mesiac: r("month", "sk"), de\u0148: r("day", "sk"), \u00E5r: r("year", "sv", "da", "nb"), kvartal: r("quarter", "sv", "da", "nb"), m\u00E5nad: r("month", "sv"), m\u00E5ned: r("month", "da", "nb"), vuosi: r("year", "fi"), vuosinelj\u00E4nnes: r("quarter", "fi"), kuukausi: r("month", "fi"), p\u00E4iv\u00E4: r("day", "fi"), \u00E9v: r("year", "hu"), negyed\u00E9v: r("quarter", "hu"), h\u00F3nap: r("month", "hu"), nap: r("day", "hu"), y\u0131l: r("year", "tr"), \u00E7eyrek: r("quarter", "tr"), ay: r("month", "tr"), g\u00FCn: r("day", "tr"), \u03AD\u03C4\u03BF\u03C2: r("year", "el"), \u03C4\u03C1\u03AF\u03BC\u03B7\u03BD\u03BF: r("quarter", "el"), \u03BC\u03AE\u03BD\u03B1\u03C2: r("month", "el"), \u03B7\u03BC\u03AD\u03C1\u03B1: r("day", "el"), an: r("year", "ro"), trimestru: r("quarter", "ro"), lun\u0103: r("month", "ro"), zi: r("day", "ro"), godina: r("year", "hr"), tromjese\u010Dje: r("quarter", "hr"), mjesec: r("month", "hr"), dan: r("day", "hr"), \u5E74: r("year", "ja", "zh"), \u56DB\u534A\u671F: r("quarter", "ja"), \u5B63\u5EA6: r("quarter", "zh"), \u6708: r("month", "ja", "zh"), \u65E5: r("day", "ja", "zh"), \uC5F0\uB3C4: r("year", "ko"), \uBD84\uAE30: r("quarter", "ko"), \uC6D4: r("month", "ko"), \uC77C: r("day", "ko"), tahun: r("year", "id"), kuartal: r("quarter", "id"), triwulan: r("quarter", "id"), bulan: r("month", "id"), hari: r("day", "id"), n\u0103m: r("year", "vi"), qu\u00FD: r("quarter", "vi"), th\u00E1ng: r("month", "vi"), ng\u00E0y: r("day", "vi"), \u0E1B\u0E35: r("year", "th"), \u0E44\u0E15\u0E23\u0E21\u0E32\u0E2A: r("quarter", "th"), \u0E40\u0E14\u0E37\u0E2D\u0E19: r("month", "th"), \u0E27\u0E31\u0E19: r("day", "th"), \u0627\u0644\u0633\u0646\u0629: r("year", "ar"), \u0633\u0646\u0629: r("year", "ar"), \u0627\u0644\u0639\u0627\u0645: r("year", "ar"), \u0639\u0627\u0645: r("year", "ar"), \u0627\u0644\u0631\u0628\u0639: r("quarter", "ar"), \u0631\u0628\u0639: r("quarter", "ar"), "\u0631\u0628\u0639 \u0627\u0644\u0633\u0646\u0629": r("quarter", "ar"), \u0627\u0644\u0634\u0647\u0631: r("month", "ar"), \u0634\u0647\u0631: r("month", "ar"), \u0627\u0644\u064A\u0648\u0645: r("day", "ar"), \u064A\u0648\u0645: r("day", "ar"), \u05E9\u05E0\u05D4: r("year", "he"), \u05E8\u05D1\u05E2\u05D5\u05DF: r("quarter", "he"), \u05D7\u05D5\u05D3\u05E9: r("month", "he"), \u05D9\u05D5\u05DD: r("day", "he"), \u0935\u0930\u094D\u0937: r("year", "hi"), \u0938\u093E\u0932: r("year", "hi"), \u0924\u093F\u092E\u093E\u0939\u0940: r("quarter", "hi"), \u092E\u0939\u0940\u0928\u093E: r("month", "hi"), \u092E\u093E\u0939: r("month", "hi"), \u0926\u093F\u0928: r("day", "hi"), \u0633\u0627\u0644: r("year", "fa"), \u0645\u0627\u0647: r("month", "fa"), \u0631\u0648\u0632: r("day", "fa") });
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
formatSourceDateFor,
|
|
6
6
|
isExcelDateFormat,
|
|
7
7
|
normaliseAggregation
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-4KCSAHL5.mjs";
|
|
9
9
|
import {
|
|
10
10
|
ANNOTATION_CLASS,
|
|
11
11
|
ANNOTATION_LAYER_CLASS,
|
|
@@ -111,12 +111,12 @@ import {
|
|
|
111
111
|
toSourceRows,
|
|
112
112
|
unpinScrolledAxis,
|
|
113
113
|
valuePlacementFlag
|
|
114
|
-
} from "./chunk-
|
|
114
|
+
} from "./chunk-CJNONH6V.mjs";
|
|
115
115
|
import {
|
|
116
116
|
_3,
|
|
117
117
|
l,
|
|
118
118
|
o
|
|
119
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-2XWE6YFO.mjs";
|
|
120
120
|
import {
|
|
121
121
|
MIN_HIT_BAND_PX,
|
|
122
122
|
SELECTION_GLYPH_CLASS,
|
|
@@ -2073,6 +2073,11 @@ function markerArmedBlind(m2) {
|
|
|
2073
2073
|
if (!m2) return false;
|
|
2074
2074
|
return (m2.h ?? m2.v - 1) <= 0;
|
|
2075
2075
|
}
|
|
2076
|
+
function markerArmedAtMs(m2) {
|
|
2077
|
+
if (!m2) return void 0;
|
|
2078
|
+
const t2 = Math.floor(m2.t);
|
|
2079
|
+
return Number.isFinite(t2) && t2 > 0 ? t2 : void 0;
|
|
2080
|
+
}
|
|
2076
2081
|
function isBlindMarker(m2) {
|
|
2077
2082
|
if (!m2) return false;
|
|
2078
2083
|
return typeof m2.c !== "string" || m2.c.trim() === "";
|
|
@@ -2131,10 +2136,11 @@ function recoveryFinalOutcome(r2) {
|
|
|
2131
2136
|
if (!r2) return null;
|
|
2132
2137
|
const o3 = String(r2.pollOutcome ?? "").trim().toLowerCase();
|
|
2133
2138
|
if (r2.isGenerationCancelled === true || r2.isPollFinal === true && o3 === "cancelled") return "cancelled";
|
|
2134
|
-
if (r2.isPollFinal === true) return o3 === "not-received" ? "not-received" : "failed";
|
|
2139
|
+
if (r2.isPollFinal === true) return o3 === "not-received" ? "not-received" : o3 === "lost-in-restart" ? "lost-in-restart" : "failed";
|
|
2135
2140
|
if (r2.isPollFinal == null && r2.isVersionNotFound === true) {
|
|
2136
2141
|
const c2 = String(r2.errorCode ?? "").trim().toUpperCase();
|
|
2137
2142
|
if (c2 === "GENERATION_NOT_RECEIVED") return "not-received";
|
|
2143
|
+
if (c2 === "GENERATION_LOST_IN_RESTART") return "lost-in-restart";
|
|
2138
2144
|
if (c2 === "GENERATION_FAILED") return "failed";
|
|
2139
2145
|
if (c2 === "GENERATION_CANCELLED") return "cancelled";
|
|
2140
2146
|
}
|
|
@@ -2152,11 +2158,12 @@ function recoveryLooksUnreachable(transportFailuresInARow, everAnswered) {
|
|
|
2152
2158
|
}
|
|
2153
2159
|
var GENERATION_NOT_RECEIVED_MESSAGE = "The charting service has no record of that request, so nothing was generated and nothing was charged. Generate again.";
|
|
2154
2160
|
var GENERATION_FAILED_MESSAGE = "That chart didn't finish on the server, so there's nothing to show. Generate again.";
|
|
2161
|
+
var GENERATION_LOST_IN_RESTART_MESSAGE = "The charting service restarted while your chart was being built, so nothing was delivered. Click Generate to build it again.";
|
|
2155
2162
|
var RECOVERY_UNREACHABLE_MESSAGE = "We couldn't reach the charting service to check on your chart - it may be offline, or blocked on this network. Try again in a moment.";
|
|
2156
2163
|
function recoveryFinalMessage(o3, serverMessage) {
|
|
2157
2164
|
const s2 = String(serverMessage ?? "").trim();
|
|
2158
2165
|
if (s2 !== "") return s2;
|
|
2159
|
-
return o3 === "cancelled" ? GENERATION_CANCELLED_MESSAGE : o3 === "not-received" ? GENERATION_NOT_RECEIVED_MESSAGE : GENERATION_FAILED_MESSAGE;
|
|
2166
|
+
return o3 === "cancelled" ? GENERATION_CANCELLED_MESSAGE : o3 === "not-received" ? GENERATION_NOT_RECEIVED_MESSAGE : o3 === "lost-in-restart" ? GENERATION_LOST_IN_RESTART_MESSAGE : GENERATION_FAILED_MESSAGE;
|
|
2160
2167
|
}
|
|
2161
2168
|
|
|
2162
2169
|
// src/chartPick.ts
|
|
@@ -2339,6 +2346,7 @@ export {
|
|
|
2339
2346
|
FLIP_MODE_DEFAULT,
|
|
2340
2347
|
GENERATION_CANCELLED_MESSAGE,
|
|
2341
2348
|
GENERATION_FAILED_MESSAGE,
|
|
2349
|
+
GENERATION_LOST_IN_RESTART_MESSAGE,
|
|
2342
2350
|
GENERATION_NOT_RECEIVED_MESSAGE,
|
|
2343
2351
|
GEO_POINT_PRECISIONS,
|
|
2344
2352
|
GLYPH_SAMPLE_N,
|
|
@@ -2498,6 +2506,7 @@ export {
|
|
|
2498
2506
|
loadGeo,
|
|
2499
2507
|
markDefaultModel,
|
|
2500
2508
|
markKeyOf,
|
|
2509
|
+
markerArmedAtMs,
|
|
2501
2510
|
markerArmedBlind,
|
|
2502
2511
|
measureContainerBoxes,
|
|
2503
2512
|
minContrastOver,
|
package/dist/react.mjs
CHANGED
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
loadGeo,
|
|
23
23
|
syncMemberSelection,
|
|
24
24
|
toSourceRows
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import "./chunk-
|
|
25
|
+
} from "./chunk-CJNONH6V.mjs";
|
|
26
|
+
import "./chunk-2XWE6YFO.mjs";
|
|
27
27
|
import "./chunk-MGAO2DR7.mjs";
|
|
28
28
|
import "./chunk-A2GMXZP7.mjs";
|
|
29
29
|
import "./chunk-7JWL6LYE.mjs";
|
package/dist/testing.mjs
CHANGED
|
@@ -100,9 +100,186 @@ async function assertMarkerStoreConformance(store) {
|
|
|
100
100
|
}
|
|
101
101
|
done();
|
|
102
102
|
}
|
|
103
|
+
function isPlainBag(v) {
|
|
104
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
105
|
+
}
|
|
106
|
+
function canonical(v) {
|
|
107
|
+
return JSON.stringify(v, (_k, val) => isPlainBag(val) ? Object.fromEntries(Object.keys(val).sort().map((k) => [k, val[k]])) : val);
|
|
108
|
+
}
|
|
109
|
+
function isProvider(p) {
|
|
110
|
+
return !!p && typeof p.load === "function" && typeof p.save === "function";
|
|
111
|
+
}
|
|
112
|
+
function policyEntryFailures(where, entry) {
|
|
113
|
+
if (!isPlainBag(entry)) return [`${where} is not an object`];
|
|
114
|
+
const out = [];
|
|
115
|
+
if (entry.lifetime !== "viewing" && entry.lifetime !== "durable") {
|
|
116
|
+
out.push(`${where}.lifetime is ${JSON.stringify(entry.lifetime)}, not "viewing" or "durable"`);
|
|
117
|
+
}
|
|
118
|
+
if (typeof entry.dropOnNewVersion !== "boolean") out.push(`${where}.dropOnNewVersion is not a boolean`);
|
|
119
|
+
if (typeof entry.dropOnFreshViewing !== "boolean") out.push(`${where}.dropOnFreshViewing is not a boolean`);
|
|
120
|
+
if (entry.durableWhen !== void 0 && (typeof entry.durableWhen !== "string" || !entry.durableWhen)) {
|
|
121
|
+
out.push(`${where}.durableWhen is not a preference name`);
|
|
122
|
+
}
|
|
123
|
+
if (entry.lifetime === "viewing" && entry.durableWhen !== void 0) {
|
|
124
|
+
out.push(`${where} has a durableWhen but a "viewing" lifetime, so the preference could never matter`);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function shapeFailures(service) {
|
|
129
|
+
if (!service || typeof service !== "object") return ["the service is missing"];
|
|
130
|
+
const out = [];
|
|
131
|
+
if (!isProvider(service.viewing)) out.push("viewing is missing, or has no load() or save()");
|
|
132
|
+
if (service.durable != null && !isProvider(service.durable)) out.push("durable has no load() or save()");
|
|
133
|
+
if (!isPlainBag(service.policy)) out.push("policy is missing, or is not an object keyed by view-state key");
|
|
134
|
+
else for (const k of Object.keys(service.policy)) out.push(...policyEntryFailures(`policy.${k}`, service.policy[k]));
|
|
135
|
+
if (service.chartKeys != null) out.push(...policyEntryFailures("chartKeys", service.chartKeys));
|
|
136
|
+
if (service.versionKey != null && (typeof service.versionKey !== "string" || !service.versionKey)) {
|
|
137
|
+
out.push("versionKey is not a key name");
|
|
138
|
+
}
|
|
139
|
+
if (service.durableMaxChars != null && !(typeof service.durableMaxChars === "number" && Number.isInteger(service.durableMaxChars) && service.durableMaxChars > 0)) {
|
|
140
|
+
out.push("durableMaxChars is not a positive whole number");
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
var PROBE_FIRST = {
|
|
145
|
+
__bicharts_conformance_view_a__: 1,
|
|
146
|
+
nested: { list: [1, "two", { three: 3 }], flag: true, none: null },
|
|
147
|
+
text: 'caf\xE9 "quoted"'
|
|
148
|
+
};
|
|
149
|
+
var PROBE_SECOND = { __bicharts_conformance_view_b__: ["only", "this"] };
|
|
150
|
+
async function assertViewStateConformance(service, options = {}) {
|
|
151
|
+
const failures = [];
|
|
152
|
+
const checked = [];
|
|
153
|
+
const skipped = [];
|
|
154
|
+
const finish = () => {
|
|
155
|
+
if (failures.length) throw new ConformanceError("ViewStateService", failures);
|
|
156
|
+
return { checked, skipped };
|
|
157
|
+
};
|
|
158
|
+
const broken = shapeFailures(service);
|
|
159
|
+
if (broken.length) {
|
|
160
|
+
failures.push(...broken);
|
|
161
|
+
finish();
|
|
162
|
+
}
|
|
163
|
+
checked.push("shape");
|
|
164
|
+
const flush = async (durable) => {
|
|
165
|
+
if (durable && options.settle) await options.settle();
|
|
166
|
+
};
|
|
167
|
+
const expectBag = (label, what, got, want) => {
|
|
168
|
+
if (!isPlainBag(got)) failures.push(`${label}: ${what} returned ${canonical(got) ?? String(got)}, not an object`);
|
|
169
|
+
else if (canonical(got) !== canonical(want)) {
|
|
170
|
+
failures.push(`${label}: ${what} returned ${canonical(got)}, not ${canonical(want)}`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const checkStore = async (label, store, durable) => {
|
|
174
|
+
let before = "{}";
|
|
175
|
+
try {
|
|
176
|
+
const held = store.load();
|
|
177
|
+
if (!isPlainBag(held)) failures.push(`${label}: load() of a store with nothing set returned ${String(held)}, not an object`);
|
|
178
|
+
else before = canonical(held);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
failures.push(`${label}: load() threw, and it must never: ${describeThrow(e)}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
store.save({});
|
|
185
|
+
await flush(durable);
|
|
186
|
+
expectBag(label, "load() after save({})", store.load(), {});
|
|
187
|
+
store.save(JSON.parse(canonical(PROBE_FIRST)));
|
|
188
|
+
await flush(durable);
|
|
189
|
+
expectBag(label, "load() after save()", store.load(), PROBE_FIRST);
|
|
190
|
+
store.save(JSON.parse(canonical(PROBE_SECOND)));
|
|
191
|
+
await flush(durable);
|
|
192
|
+
const replaced = store.load();
|
|
193
|
+
if (isPlainBag(replaced) && canonical(replaced) !== canonical(PROBE_SECOND)) {
|
|
194
|
+
failures.push(`${label}: a second save() did not replace the first (a save takes the whole bag): load() returned ${canonical(replaced)}`);
|
|
195
|
+
} else if (!isPlainBag(replaced)) {
|
|
196
|
+
failures.push(`${label}: load() after a second save() returned ${String(replaced)}, not an object`);
|
|
197
|
+
}
|
|
198
|
+
store.save({});
|
|
199
|
+
await flush(durable);
|
|
200
|
+
const emptied = store.load();
|
|
201
|
+
if (!isPlainBag(emptied) || Object.keys(emptied).length) {
|
|
202
|
+
failures.push(`${label}: save({}) did not empty the store: load() returned ${canonical(emptied)}`);
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
failures.push(`${label}: threw during an ordinary load() or save(): ${describeThrow(e)}`);
|
|
206
|
+
} finally {
|
|
207
|
+
try {
|
|
208
|
+
store.save(JSON.parse(before));
|
|
209
|
+
await flush(durable);
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
await checkStore("viewing", service.viewing, false);
|
|
215
|
+
checked.push("viewing: load, save, replace, round trip");
|
|
216
|
+
if (service.durable) {
|
|
217
|
+
await checkStore("durable", service.durable, true);
|
|
218
|
+
checked.push("durable: load, save, replace, round trip");
|
|
219
|
+
}
|
|
220
|
+
if (!options.recreate) {
|
|
221
|
+
skipped.push("the viewing store survives a host re-created on the same element (pass options.recreate)");
|
|
222
|
+
} else {
|
|
223
|
+
let before = "{}";
|
|
224
|
+
try {
|
|
225
|
+
before = canonical(service.viewing.load());
|
|
226
|
+
service.viewing.save(JSON.parse(canonical(PROBE_FIRST)));
|
|
227
|
+
const again = options.recreate();
|
|
228
|
+
if (!again || !isProvider(again.viewing)) failures.push("viewing: recreate() did not return a service with a viewing store");
|
|
229
|
+
else expectBag("viewing", "load() on a service re-created over the same element", again.viewing.load(), PROBE_FIRST);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
failures.push(`viewing: threw while checking a re-created service: ${describeThrow(e)}`);
|
|
232
|
+
} finally {
|
|
233
|
+
try {
|
|
234
|
+
service.viewing.save(JSON.parse(before));
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
checked.push("viewing: survives a host re-created on the same element");
|
|
239
|
+
}
|
|
240
|
+
if (service.durable) {
|
|
241
|
+
if (!options.reopen) {
|
|
242
|
+
skipped.push("the durable store survives a reopen and the viewing store starts clean (pass options.reopen)");
|
|
243
|
+
} else {
|
|
244
|
+
let before = "{}";
|
|
245
|
+
let viewingBefore = "{}";
|
|
246
|
+
try {
|
|
247
|
+
before = canonical(service.durable.load());
|
|
248
|
+
viewingBefore = canonical(service.viewing.load());
|
|
249
|
+
service.viewing.save(JSON.parse(canonical(PROBE_SECOND)));
|
|
250
|
+
service.durable.save(JSON.parse(canonical(PROBE_FIRST)));
|
|
251
|
+
await flush(true);
|
|
252
|
+
const reopened = options.reopen();
|
|
253
|
+
if (!reopened || !isProvider(reopened.durable)) {
|
|
254
|
+
failures.push("durable: reopen() did not return a service with a durable store");
|
|
255
|
+
} else {
|
|
256
|
+
expectBag("durable", "load() on a service reopened over the same saved file", reopened.durable.load(), PROBE_FIRST);
|
|
257
|
+
}
|
|
258
|
+
if (reopened && isProvider(reopened.viewing)) {
|
|
259
|
+
expectBag("viewing", "load() after a reopen (a viewing starts clean)", reopened.viewing.load(), {});
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
failures.push(`durable: threw while checking a reopened service: ${describeThrow(e)}`);
|
|
263
|
+
} finally {
|
|
264
|
+
try {
|
|
265
|
+
service.durable.save(JSON.parse(before));
|
|
266
|
+
await flush(true);
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
service.viewing.save(JSON.parse(viewingBefore));
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
checked.push("durable: survives a reopen; viewing: starts clean after one");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return finish();
|
|
278
|
+
}
|
|
103
279
|
export {
|
|
104
280
|
ConformanceError,
|
|
105
281
|
assertMarkerStoreConformance,
|
|
282
|
+
assertViewStateConformance,
|
|
106
283
|
chartContainers,
|
|
107
284
|
clickEmpty,
|
|
108
285
|
clickMark,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RenderOptions } from "../contract";
|
|
1
|
+
import type { RenderOptions, ViewStateProvider } from "../contract";
|
|
2
2
|
/** Signs an encoded request body. See @bicharts/shape-core's host/services.ts. */
|
|
3
3
|
export interface WireSigner {
|
|
4
4
|
sign(encodedBody: string): string;
|
|
@@ -112,6 +112,71 @@ export interface ThumbnailConsent {
|
|
|
112
112
|
export interface SnapshotTarget {
|
|
113
113
|
svg(): SVGSVGElement | null;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* How one key of the view-state bag lives and dies. A chart reads and writes its resting state
|
|
117
|
+
* (a sort, a frame, a 3D camera) through `options.uiState` / `options.setUiState`; the host
|
|
118
|
+
* decides what each key is allowed to outlive. A key that is not named in a service's `policy`
|
|
119
|
+
* is the chart's own, and follows `ViewStateService.chartKeys`.
|
|
120
|
+
*/
|
|
121
|
+
export interface ViewStateKeyPolicy {
|
|
122
|
+
/**
|
|
123
|
+
* `"viewing"`: kept for this viewing only (it survives a redraw, never reaches the durable
|
|
124
|
+
* store, and a durable store that happens to hold it is not believed). `"durable"`: kept in
|
|
125
|
+
* the viewing store AND the durable store, so it survives a close and reopen.
|
|
126
|
+
*/
|
|
127
|
+
lifetime: "viewing" | "durable";
|
|
128
|
+
/**
|
|
129
|
+
* Drop the key when the chart on screen is a different version from the one the key was set
|
|
130
|
+
* on. A 3D camera is aimed at one chart's axes; the next chart has no use for it.
|
|
131
|
+
*/
|
|
132
|
+
dropOnNewVersion: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Drop the key when the bag is read at the start of a fresh viewing (a page opened, a file
|
|
135
|
+
* opened) instead of in the viewing that wrote it. A manual "stop" on an animation belongs
|
|
136
|
+
* to the viewing that made it, though the durable store may hold it.
|
|
137
|
+
*/
|
|
138
|
+
dropOnFreshViewing: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* A preference that must read true for this key to reach the durable store. When it reads
|
|
141
|
+
* anything else, the key still lives in the viewing store (the reader keeps their view while
|
|
142
|
+
* the page is open) and stays out of the durable one. An unset preference reads as off.
|
|
143
|
+
*/
|
|
144
|
+
durableWhen?: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Where a host keeps a chart's resting view-state, and what each key may outlive. "Render"
|
|
148
|
+
* lifetime is the absence of a store: anything not saved dies with the redraw.
|
|
149
|
+
*
|
|
150
|
+
* `viewing` is required; `durable` is optional, and a host that has none (a static preview, a
|
|
151
|
+
* server-side render) is saying it cannot remember across a close. Every member besides the two
|
|
152
|
+
* stores is a rule the shared view-state functions apply; the stores themselves stay dumb.
|
|
153
|
+
*/
|
|
154
|
+
export interface ViewStateService {
|
|
155
|
+
/** Survives a redraw (a resize, a cross-filter, a settings change). A page switch or a reopen starts clean. */
|
|
156
|
+
viewing: ViewStateProvider;
|
|
157
|
+
/** Survives close and reopen (a report file, a workbook). Absent or null: this host cannot. */
|
|
158
|
+
durable?: ViewStateProvider | null;
|
|
159
|
+
/** The keys the HOST writes or scopes, by name. */
|
|
160
|
+
policy: Readonly<Record<string, ViewStateKeyPolicy>>;
|
|
161
|
+
/**
|
|
162
|
+
* The rule for every key not named in `policy`: the ones the chart's own code writes.
|
|
163
|
+
* Absent: they are the chart's to keep (durable, and they survive a new version and a fresh
|
|
164
|
+
* viewing), because the chart validates what it reads. A host that forgets everything when
|
|
165
|
+
* the chart changes sets `dropOnNewVersion` here.
|
|
166
|
+
*/
|
|
167
|
+
chartKeys?: ViewStateKeyPolicy | null;
|
|
168
|
+
/**
|
|
169
|
+
* The key under which the bag records the chart version it was last read for, so a version
|
|
170
|
+
* change is noticed even by a host that never saw the swap. Absent: the host applies the
|
|
171
|
+
* version rule itself, at the swap.
|
|
172
|
+
*/
|
|
173
|
+
versionKey?: string | null;
|
|
174
|
+
/**
|
|
175
|
+
* The most characters of JSON the durable store accepts. A larger write is refused whole:
|
|
176
|
+
* nothing is saved, in either store, so the next read sees the state before it. Absent: no limit.
|
|
177
|
+
*/
|
|
178
|
+
durableMaxChars?: number | null;
|
|
179
|
+
}
|
|
115
180
|
/**
|
|
116
181
|
* The whole services object a host constructs and passes to shared logic.
|
|
117
182
|
*
|
|
@@ -128,6 +193,9 @@ export interface SnapshotTarget {
|
|
|
128
193
|
* - theme: visual, add-in; a web page when it reads the browser's scheme; not the MCP server.
|
|
129
194
|
* - thumbnails and snapshot: visual, add-in; not the MCP server (no DOM) or a demo page.
|
|
130
195
|
* - diagnostics: visual, add-in; not the MCP server (stdio) or a demo page.
|
|
196
|
+
* - viewState: visual, add-in and a web page that passes a provider; the MCP preview passes an
|
|
197
|
+
* explicit no-op service. A host can remember across a close exactly when `viewState.durable`
|
|
198
|
+
* is present (see `viewStateIsDurable` in "@bicharts/chart-host/view-state").
|
|
131
199
|
*/
|
|
132
200
|
export interface HostServices {
|
|
133
201
|
signer: WireSigner;
|
|
@@ -142,4 +210,5 @@ export interface HostServices {
|
|
|
142
210
|
thumbnails?: ThumbnailConsent | null;
|
|
143
211
|
snapshot?: SnapshotTarget | null;
|
|
144
212
|
diagnostics?: DiagnosticsSink | null;
|
|
213
|
+
viewState?: ViewStateService | null;
|
|
145
214
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export { pinScrolledAxis, unpinScrolledAxis, type AxisPinReport } from "./fitDom
|
|
|
44
44
|
export { MIN_DELTA_E, pickDistinctColorFromSeed, buildPalette, buildPaletteFromSeed, orderMostDistinct, LEGIBLE_LIGHTNESS, type PaletteResult, type SeedForSlot, type PaletteOptions, } from "./palette";
|
|
45
45
|
export { deltaEHex } from "./deltaE";
|
|
46
46
|
export { resolveAutoColorScale, autoRampMinStep, autoRampCanvas, AUTO_RAMP_CLASSES, AUTO_RAMP_DRAWN_OPACITY, AUTO_RAMP_MIN_STEP_DELTA_E, type AutoColorScale, type AutoColorScaleInput } from "./autoRamp";
|
|
47
|
-
export { PENDING_RECOVERY_WINDOW_MS, PENDING_RECOVERY_POLL_MS, PENDING_RECOVERY_FIRST_POLL_MS, PENDING_RECOVERY_FAST_PHASE_MS, PENDING_RECOVERY_MID_POLL_MS, PENDING_RECOVERY_SLOW_PHASE_MS, PENDING_RECOVERY_SLOW_POLL_MS, pendingRecoveryNextDelayMs, encodePendingGenerate, parsePendingGenerate, markerArmedBlind, isBlindMarker, servedCorrelationProves, sameMarker, decidePendingRecovery, recoveryPollRunning, recoveryOwnsTheSentence, latePickupAction, latePickupKeepsMarker, pollGiveUpKeepsMarker, pendingRecoveryRemainingMs, transportFailureShouldRecover, TRANSPORT_LOST_CHECKING_MESSAGE, GENERATION_CANCELLED_MESSAGE, recoveryAnswerIsCancelled, generationCancelledMessage, recoveryFinalOutcome, recoveryProvesNothingCharged, recoveryGiveUpRefunds, recoveryLooksUnreachable, recoveryFinalMessage, PENDING_RECOVERY_UNREACHABLE_POLLS, GENERATION_NOT_RECEIVED_MESSAGE, GENERATION_FAILED_MESSAGE, RECOVERY_UNREACHABLE_MESSAGE, type PendingGenerateMarker, type PendingRecoveryDecision, type LatePickupAction, type RecoveryFinalOutcome, } from "./pendingGenerate";
|
|
47
|
+
export { PENDING_RECOVERY_WINDOW_MS, PENDING_RECOVERY_POLL_MS, PENDING_RECOVERY_FIRST_POLL_MS, PENDING_RECOVERY_FAST_PHASE_MS, PENDING_RECOVERY_MID_POLL_MS, PENDING_RECOVERY_SLOW_PHASE_MS, PENDING_RECOVERY_SLOW_POLL_MS, pendingRecoveryNextDelayMs, encodePendingGenerate, parsePendingGenerate, markerArmedBlind, markerArmedAtMs, isBlindMarker, servedCorrelationProves, sameMarker, decidePendingRecovery, recoveryPollRunning, recoveryOwnsTheSentence, latePickupAction, latePickupKeepsMarker, pollGiveUpKeepsMarker, pendingRecoveryRemainingMs, transportFailureShouldRecover, TRANSPORT_LOST_CHECKING_MESSAGE, GENERATION_CANCELLED_MESSAGE, recoveryAnswerIsCancelled, generationCancelledMessage, recoveryFinalOutcome, recoveryProvesNothingCharged, recoveryGiveUpRefunds, recoveryLooksUnreachable, recoveryFinalMessage, PENDING_RECOVERY_UNREACHABLE_POLLS, GENERATION_NOT_RECEIVED_MESSAGE, GENERATION_FAILED_MESSAGE, GENERATION_LOST_IN_RESTART_MESSAGE, RECOVERY_UNREACHABLE_MESSAGE, type PendingGenerateMarker, type PendingRecoveryDecision, type LatePickupAction, type RecoveryFinalOutcome, } from "./pendingGenerate";
|
|
48
48
|
export { stripJsComments } from "./codeComments";
|
|
49
49
|
export { reconcileChartPick, type ChartPickReconciliation } from "./chartPick";
|
|
50
50
|
export { isDuplicateGenerate, armGenerateGuard, DUPLICATE_GENERATE_WINDOW_MS, type GenerateGuardKey, type GenerateGuardState, } from "./duplicateGenerateGuard";
|
|
@@ -53,4 +53,4 @@ export { isNarrowedByFilter, isEmptiedByFilter, updateRowHighWater, type Narrowi
|
|
|
53
53
|
export { shouldCaptureThumbnail, type ThumbnailGate, type ThumbnailDecision } from "./thumbnailPolicy";
|
|
54
54
|
export { isPickableModelCode, markDefaultModel, defaultModelFirst, reconcileModelPick, DEFAULT_MODEL_SUFFIX, } from "./modelPicker";
|
|
55
55
|
export { stableFingerprintOptions, fingerprintComponentDigest, type ClientIdSource } from "./clientIdentity";
|
|
56
|
-
export type { HostServices, MarkerStore, PreferenceSource, HostTheme, ThemeSource, ThumbnailConsent, SnapshotTarget, WireSigner, WireTransport, WireResponse, CredentialTriple, CredentialSource, ViewportSource, Clock, DiagnosticEntry, DiagnosticsSink, RendererId, } from "./host/services";
|
|
56
|
+
export type { HostServices, MarkerStore, PreferenceSource, HostTheme, ThemeSource, ThumbnailConsent, SnapshotTarget, WireSigner, WireTransport, WireResponse, CredentialTriple, CredentialSource, ViewportSource, Clock, DiagnosticEntry, DiagnosticsSink, RendererId, ViewStateKeyPolicy, ViewStateService, } from "./host/services";
|
|
@@ -55,6 +55,17 @@ export type PendingRecoveryDecision = "none" | "delivered" | "expired" | "blind"
|
|
|
55
55
|
* `v - 1` fallback says the same thing for markers written by older builds.
|
|
56
56
|
*/
|
|
57
57
|
export declare function markerArmedBlind(m: PendingGenerateMarker | null | undefined): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* WHEN THIS MARKER'S GENERATE STARTED, as a recovery poll states it: the marker's own `t`, epoch milliseconds on
|
|
60
|
+
* this host's clock, as a whole number - the `fetchArmedAtMs` a poll by correlation passes to `fetchFields`.
|
|
61
|
+
*
|
|
62
|
+
* With it the server can tell a generate that died with a server restart (started before the running process,
|
|
63
|
+
* no record of it) from one still running, and say so at the first poll instead of after the whole window.
|
|
64
|
+
* Nothing is shifted or checked against the clock here: a slow or fast host clock is the server's margin to
|
|
65
|
+
* absorb, and a future time it reads as absent. undefined for no marker or a time that is not a positive
|
|
66
|
+
* number, so the poll sends exactly what it always sent.
|
|
67
|
+
*/
|
|
68
|
+
export declare function markerArmedAtMs(m: PendingGenerateMarker | null | undefined): number | undefined;
|
|
58
69
|
/**
|
|
59
70
|
* Can this marker be recovered AT ALL? Only by CORRELATION, so only if it carries one.
|
|
60
71
|
*
|
|
@@ -198,7 +209,7 @@ export declare function generationCancelledMessage(serverMessage: string | null
|
|
|
198
209
|
* beside it for older hosts, so this is read FIRST. null = not final. A final answer with an outcome
|
|
199
210
|
* this build does not know reads as "failed": it is final, and it claims nothing about charges.
|
|
200
211
|
*/
|
|
201
|
-
export type RecoveryFinalOutcome = "cancelled" | "failed" | "not-received";
|
|
212
|
+
export type RecoveryFinalOutcome = "cancelled" | "failed" | "not-received" | "lost-in-restart";
|
|
202
213
|
export declare function recoveryFinalOutcome(r: {
|
|
203
214
|
isPollFinal?: boolean | null;
|
|
204
215
|
pollOutcome?: string | null;
|
|
@@ -207,7 +218,8 @@ export declare function recoveryFinalOutcome(r: {
|
|
|
207
218
|
errorCode?: string | null;
|
|
208
219
|
} | null | undefined): RecoveryFinalOutcome | null;
|
|
209
220
|
/** Did the server PROVE nothing was charged? Only a cancel and a request it never received do; a
|
|
210
|
-
* failed generation's charge is its own, and a
|
|
221
|
+
* failed generation's charge is its own, and a generate that died with a restart may have been charged
|
|
222
|
+
* before its record was written, so a host claims nothing about either. */
|
|
211
223
|
export declare function recoveryProvesNothingCharged(o: string | null | undefined): boolean;
|
|
212
224
|
/** Does a recovery that ended this way send the refund? Only when it ended with NOTHING PAINTED and
|
|
213
225
|
* nothing proven - the window closed, the poll itself was refused, or the service could not be
|
|
@@ -222,6 +234,10 @@ export declare function recoveryLooksUnreachable(transportFailuresInARow: number
|
|
|
222
234
|
* localises by code. */
|
|
223
235
|
export declare const GENERATION_NOT_RECEIVED_MESSAGE = "The charting service has no record of that request, so nothing was generated and nothing was charged. Generate again.";
|
|
224
236
|
export declare const GENERATION_FAILED_MESSAGE = "That chart didn't finish on the server, so there's nothing to show. Generate again.";
|
|
237
|
+
/** The process that was building the chart is gone, so nothing will arrive. Says what happened and what to
|
|
238
|
+
* do, and claims nothing about a charge: a free-tier meter can be charged before the generation's record
|
|
239
|
+
* exists, so a restart in between can leave a charge that no record shows. */
|
|
240
|
+
export declare const GENERATION_LOST_IN_RESTART_MESSAGE = "The charting service restarted while your chart was being built, so nothing was delivered. Click Generate to build it again.";
|
|
225
241
|
/** Neither the generate's answer nor any check reached us. Claims nothing about a charge: behind a
|
|
226
242
|
* proxy the generate may have arrived when the checks did not. */
|
|
227
243
|
export declare const RECOVERY_UNREACHABLE_MESSAGE = "We couldn't reach the charting service to check on your chart - it may be offline, or blocked on this network. Try again in a moment.";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MarkerStore } from "../host/services";
|
|
1
|
+
import type { MarkerStore, ViewStateService } from "../host/services";
|
|
2
2
|
/** Thrown by every conformance check. `failures` names each property the adapter broke. */
|
|
3
3
|
export declare class ConformanceError extends Error {
|
|
4
4
|
readonly contract: string;
|
|
@@ -12,4 +12,43 @@ export declare class ConformanceError extends Error {
|
|
|
12
12
|
* before returning, whether or not the check passed.
|
|
13
13
|
*/
|
|
14
14
|
export declare function assertMarkerStoreConformance(store: MarkerStore): Promise<void>;
|
|
15
|
+
/** What a host can hand `assertViewStateConformance` so it can exercise more of the contract. */
|
|
16
|
+
export interface ViewStateConformanceOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Flushes a durable write that is debounced or asynchronous (a fake-timer tick, an awaited save),
|
|
19
|
+
* and is called after every write to the durable store before the check reads it back. A host whose
|
|
20
|
+
* durable `save()` lands synchronously omits it.
|
|
21
|
+
*/
|
|
22
|
+
settle?: () => void | Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Builds the service again over the same element, the way the host does on a redraw. A viewing
|
|
25
|
+
* store must read there what the first one saved. Without it that property goes unchecked, and the
|
|
26
|
+
* report says so.
|
|
27
|
+
*/
|
|
28
|
+
recreate?: () => ViewStateService;
|
|
29
|
+
/**
|
|
30
|
+
* Builds the service as a new viewing over the same saved file or workbook (a page switch, a
|
|
31
|
+
* reopen). The durable store must read what was saved, and the viewing store must start clean.
|
|
32
|
+
* Without it that property goes unchecked when there is a durable store, and the report says so.
|
|
33
|
+
*/
|
|
34
|
+
reopen?: () => ViewStateService;
|
|
35
|
+
}
|
|
36
|
+
/** What a passing run covered. A failing run throws instead. */
|
|
37
|
+
export interface ViewStateConformanceReport {
|
|
38
|
+
checked: readonly string[];
|
|
39
|
+
/** Properties that need an option the caller did not pass. A host's own test can require this to be empty. */
|
|
40
|
+
skipped: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Checks a ViewStateService against the contract. A bag's store: `load()` never throws and reads `{}`
|
|
44
|
+
* when nothing is stored; `save()` takes the WHOLE bag and replaces what was there; nested values come
|
|
45
|
+
* back as written. The viewing store must survive the host being re-created on the same element (pass
|
|
46
|
+
* `recreate`). The durable store, when there is one, must hold what it was given, including across a
|
|
47
|
+
* reopen (pass `reopen`) where the viewing store starts clean. The policy must be well formed.
|
|
48
|
+
*
|
|
49
|
+
* Whatever the stores held before the check is put back before it returns, whether or not it passed.
|
|
50
|
+
* Resolves to a report of what was covered and what needed an option that was not passed; throws a
|
|
51
|
+
* ConformanceError listing every broken property otherwise.
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertViewStateConformance(service: ViewStateService, options?: ViewStateConformanceOptions): Promise<ViewStateConformanceReport>;
|
|
15
54
|
export { chartContainers, marksOf, findMark, clickMark, clickEmpty, selectedRows } from "./interact";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ViewStateKeyPolicy, ViewStateService } from "./host/services";
|
|
2
|
+
/**
|
|
3
|
+
* A service that stores nothing: a static preview, a server-side render, a thumbnail capture,
|
|
4
|
+
* where remembering would be wrong rather than merely absent (a thumbnail must be the same picture
|
|
5
|
+
* every time it is taken). Saying so out loud differs from passing no service at all, which leaves
|
|
6
|
+
* the choice to the host. The viewing store reads `{}` always and there is no durable store.
|
|
7
|
+
*/
|
|
8
|
+
export declare function noopViewStateService(): ViewStateService;
|
|
9
|
+
/**
|
|
10
|
+
* Whether this host can remember a chart's view across a close and reopen. This is the one
|
|
11
|
+
* definition of "remembers the view": it is derived from the service, never declared beside it, so
|
|
12
|
+
* a host cannot claim it without a durable store to back the claim.
|
|
13
|
+
*/
|
|
14
|
+
export declare function viewStateIsDurable(service: ViewStateService | null | undefined): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The rule for a key the chart's own code writes, when a service names no other. The chart keeps it:
|
|
17
|
+
* it reaches the durable store, and it survives a new version of the chart and a fresh viewing.
|
|
18
|
+
* Such a key can be stale (a sort on a column the new chart doesn't have), and that is the chart's to
|
|
19
|
+
* handle: whatever it reads from the bag is untrusted, and it validates it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const CHART_OWNED_KEY_POLICY: Readonly<ViewStateKeyPolicy>;
|
|
22
|
+
/** A key a rule took out of the bag a chart is handed, and why. */
|
|
23
|
+
export interface ViewStateDrop {
|
|
24
|
+
key: string;
|
|
25
|
+
reason: "fresh-viewing" | "new-version";
|
|
26
|
+
}
|
|
27
|
+
export interface ResolveViewStateOptions {
|
|
28
|
+
/** The version of the chart about to draw. Compared with what the bag recorded under `versionKey`. */
|
|
29
|
+
codeVersion?: number | string | null;
|
|
30
|
+
/**
|
|
31
|
+
* True when this viewing has not yet written a bag of its own (a page just opened, a file just
|
|
32
|
+
* opened). False when it has, which makes the viewing store the bag even if it reads empty, so a
|
|
33
|
+
* chart that cleared its state is not handed the older state the durable store still holds.
|
|
34
|
+
* Absent: a bag read from the durable store is a fresh viewing, and one read from the viewing store is not.
|
|
35
|
+
*/
|
|
36
|
+
freshViewing?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ResolvedViewState {
|
|
39
|
+
/** The bag to hand the chart as `options.uiState`. The caller's own copy: nothing in a store aliases it. */
|
|
40
|
+
bag: Record<string, unknown>;
|
|
41
|
+
/** Where the bag was read from. `"none"`: neither store held anything. */
|
|
42
|
+
source: "viewing" | "durable" | "none";
|
|
43
|
+
/** Every key a rule took out, in the order the rules ran, so a host can log what it did. */
|
|
44
|
+
dropped: ViewStateDrop[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* BUILD THE BAG A CHART IS HANDED.
|
|
48
|
+
*
|
|
49
|
+
* Read order: the viewing store when it holds anything (or when the viewing has already written, see
|
|
50
|
+
* `freshViewing`), otherwise the durable store, otherwise nothing. The viewing store is the newer of
|
|
51
|
+
* the two within a viewing: a durable store can lag its own writes by a round trip, and a redraw in
|
|
52
|
+
* that window must see the write, not the state before it. A key whose lifetime is `"viewing"` is
|
|
53
|
+
* never believed from the durable store.
|
|
54
|
+
*
|
|
55
|
+
* Then two rules run, in this order, each over the keys a service's policy names (and, for the keys
|
|
56
|
+
* it doesn't, over `chartKeys`):
|
|
57
|
+
* - a fresh viewing drops every key marked `dropOnFreshViewing` whose value is set. A value that is
|
|
58
|
+
* already off (false, 0, "") stays: dropping it would change nothing the chart can tell.
|
|
59
|
+
* - a version change drops every key marked `dropOnNewVersion` that is present, and then records the
|
|
60
|
+
* current version under `versionKey` so the bag no longer reads as another chart's. Only when
|
|
61
|
+
* the service has a `versionKey`; a host without one applies the rule itself, with
|
|
62
|
+
* `viewStateAfterSwap`, wherever it learns the chart changed.
|
|
63
|
+
*
|
|
64
|
+
* The stamp is part of the bag the chart receives. The chart's own write carries it forward; a write
|
|
65
|
+
* that replaces the bag without it reads as another chart's on the next call.
|
|
66
|
+
*
|
|
67
|
+
* Never throws, and never changes a store: the bag is a copy the caller may let the chart mutate.
|
|
68
|
+
* A saved value that is not an object (a number, an array) is handed on as saved, with no rule run.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveViewState(service: ViewStateService, options?: ResolveViewStateOptions): ResolvedViewState;
|
|
71
|
+
/**
|
|
72
|
+
* WHEN THE CHART CHANGES UNDER A BAG THAT KEPT NO VERSION. Returns a copy of `bag` without the keys the
|
|
73
|
+
* policy marks `dropOnNewVersion` (and, if `chartKeys` marks it, without every key the chart wrote). A
|
|
74
|
+
* host that sees the swap itself (a regenerate, a version load, a cleared chart) calls it where it
|
|
75
|
+
* learns of it and saves what comes back. `versionKey` is left as it is: the next `resolveViewState`
|
|
76
|
+
* brings it up to date. A bag that is not an object, or nothing, comes back as `{}`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function viewStateAfterSwap(service: ViewStateService, bag: unknown): Record<string, unknown>;
|
|
79
|
+
/** What `commitViewState` did. */
|
|
80
|
+
export interface CommitViewStateResult {
|
|
81
|
+
/**
|
|
82
|
+
* `"saved"`: handed to the stores. `"ignored"`: not an object (or not serializable), so nothing was
|
|
83
|
+
* saved anywhere. `"too-large"`: the durable payload exceeded the service's `durableMaxChars`, so
|
|
84
|
+
* nothing was saved anywhere and the next read sees the state before this write.
|
|
85
|
+
*/
|
|
86
|
+
outcome: "saved" | "ignored" | "too-large";
|
|
87
|
+
/** The viewing store's `save` ran without throwing. */
|
|
88
|
+
viewingSaved: boolean;
|
|
89
|
+
/** The durable store's `save` ran without throwing. False too when the service has no durable store. */
|
|
90
|
+
durableSaved: boolean;
|
|
91
|
+
/** Keys kept out of the durable store because their `durableWhen` preference is off, in policy order. */
|
|
92
|
+
withheld: string[];
|
|
93
|
+
/** Characters of JSON in the durable payload, measured only when the service sets a limit. */
|
|
94
|
+
durableChars: number | null;
|
|
95
|
+
/** A store that threw. The other store is still written, and the write never throws out of the chart. */
|
|
96
|
+
errors: Array<{
|
|
97
|
+
store: "viewing" | "durable";
|
|
98
|
+
error: unknown;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* What the commit asks of a preference source: the value behind a switch's name. A host's own
|
|
103
|
+
* `PreferenceSource` (HostServices.prefs) satisfies it; so does `{ get: name => settings[name] }`.
|
|
104
|
+
*/
|
|
105
|
+
export interface ViewStatePreferences {
|
|
106
|
+
get(key: string): unknown;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* DECIDE WHAT A CHART'S WRITE REACHES.
|
|
110
|
+
*
|
|
111
|
+
* `next` is the whole bag the chart wrote (a write replaces; the chart merges its own siblings). The
|
|
112
|
+
* viewing store always gets all of it, so a reader who turns a 3D cube keeps their angle while the page
|
|
113
|
+
* is open whatever they have chosen to share. The durable store gets the same bag less the keys that
|
|
114
|
+
* must not reach it: a `"viewing"` lifetime, or a `durableWhen` preference that doesn't read true. The
|
|
115
|
+
* preference is read now, when the chart writes, not when the render began; an unset preference, a
|
|
116
|
+
* missing source or a source that throws all read as off, so a view is never shared by accident.
|
|
117
|
+
*
|
|
118
|
+
* The viewing store is written first and unconditionally, so a durable store that skips a write that
|
|
119
|
+
* changes nothing in its own payload (a camera-only change with the camera withheld) cannot cost the
|
|
120
|
+
* session the new pose. The one case where neither store is written is a refusal: a durable payload
|
|
121
|
+
* over `durableMaxChars` is dropped whole, so the next read sees the state before it.
|
|
122
|
+
*
|
|
123
|
+
* Never throws. A store that throws is reported in `errors` and the other store is still written.
|
|
124
|
+
*/
|
|
125
|
+
export declare function commitViewState(next: unknown, service: ViewStateService, prefs?: ViewStatePreferences | null): CommitViewStateResult;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
|
|
2
|
+
import "./chunk-GHODWOAL.mjs";
|
|
3
|
+
|
|
4
|
+
// src/viewState.ts
|
|
5
|
+
function noopViewStateService() {
|
|
6
|
+
const nowhere = { load: () => ({}), save: () => {
|
|
7
|
+
} };
|
|
8
|
+
return { viewing: nowhere, durable: null, policy: {} };
|
|
9
|
+
}
|
|
10
|
+
function viewStateIsDurable(service) {
|
|
11
|
+
return !!service && !!service.durable;
|
|
12
|
+
}
|
|
13
|
+
var CHART_OWNED_KEY_POLICY = Object.freeze({
|
|
14
|
+
lifetime: "durable",
|
|
15
|
+
dropOnNewVersion: false,
|
|
16
|
+
dropOnFreshViewing: false
|
|
17
|
+
});
|
|
18
|
+
var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
19
|
+
function isBag(v) {
|
|
20
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
function holds(v) {
|
|
23
|
+
return isBag(v) ? Object.keys(v).length > 0 : !!v;
|
|
24
|
+
}
|
|
25
|
+
function loadOrEmpty(store) {
|
|
26
|
+
try {
|
|
27
|
+
const bag = store ? store.load() : void 0;
|
|
28
|
+
return bag == null ? {} : bag;
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function put(into, key, value) {
|
|
34
|
+
Object.defineProperty(into, key, { value, enumerable: true, writable: true, configurable: true });
|
|
35
|
+
}
|
|
36
|
+
function copyOf(v, depth = 0) {
|
|
37
|
+
if (v === null || typeof v !== "object" || depth > 64) return v;
|
|
38
|
+
if (Array.isArray(v)) return v.map((x) => copyOf(x, depth + 1));
|
|
39
|
+
const proto = Object.getPrototypeOf(v);
|
|
40
|
+
if (proto !== Object.prototype && proto !== null) return v;
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const k of Object.keys(v)) put(out, k, copyOf(v[k], depth + 1));
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function policyFor(service, key) {
|
|
46
|
+
if (service.policy && own(service.policy, key)) return service.policy[key];
|
|
47
|
+
return service.chartKeys ?? CHART_OWNED_KEY_POLICY;
|
|
48
|
+
}
|
|
49
|
+
function resolveViewState(service, options = {}) {
|
|
50
|
+
const viewed = loadOrEmpty(service.viewing);
|
|
51
|
+
let source;
|
|
52
|
+
let raw;
|
|
53
|
+
if (holds(viewed) || options.freshViewing === false) {
|
|
54
|
+
source = "viewing";
|
|
55
|
+
raw = holds(viewed) ? viewed : {};
|
|
56
|
+
} else {
|
|
57
|
+
const stored = loadOrEmpty(service.durable);
|
|
58
|
+
source = holds(stored) ? "durable" : "none";
|
|
59
|
+
raw = holds(stored) ? stored : {};
|
|
60
|
+
if (isBag(raw)) {
|
|
61
|
+
const believed = {};
|
|
62
|
+
for (const k of Object.keys(raw)) if (policyFor(service, k).lifetime !== "viewing") put(believed, k, raw[k]);
|
|
63
|
+
raw = believed;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const bag = copyOf(raw);
|
|
67
|
+
const dropped = [];
|
|
68
|
+
if (!isBag(bag)) return { bag, source, dropped };
|
|
69
|
+
const versionKey = service.versionKey || null;
|
|
70
|
+
const fresh = options.freshViewing ?? source !== "viewing";
|
|
71
|
+
if (fresh) {
|
|
72
|
+
for (const key of Object.keys(bag)) {
|
|
73
|
+
if (key === versionKey) continue;
|
|
74
|
+
if (policyFor(service, key).dropOnFreshViewing && bag[key]) {
|
|
75
|
+
delete bag[key];
|
|
76
|
+
dropped.push({ key, reason: "fresh-viewing" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (versionKey) {
|
|
81
|
+
const version = options.codeVersion ?? null;
|
|
82
|
+
if (bag[versionKey] !== version) {
|
|
83
|
+
for (const key of Object.keys(bag)) {
|
|
84
|
+
if (key === versionKey) continue;
|
|
85
|
+
if (policyFor(service, key).dropOnNewVersion) {
|
|
86
|
+
delete bag[key];
|
|
87
|
+
dropped.push({ key, reason: "new-version" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
put(bag, versionKey, version);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { bag, source, dropped };
|
|
94
|
+
}
|
|
95
|
+
function viewStateAfterSwap(service, bag) {
|
|
96
|
+
const copy = copyOf(bag);
|
|
97
|
+
if (!isBag(copy)) return {};
|
|
98
|
+
const versionKey = service.versionKey || null;
|
|
99
|
+
for (const key of Object.keys(copy)) {
|
|
100
|
+
if (key !== versionKey && policyFor(service, key).dropOnNewVersion) delete copy[key];
|
|
101
|
+
}
|
|
102
|
+
return copy;
|
|
103
|
+
}
|
|
104
|
+
function preferenceOn(prefs, name) {
|
|
105
|
+
try {
|
|
106
|
+
return !!(prefs && prefs.get(name));
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function commitViewState(next, service, prefs) {
|
|
112
|
+
const result = {
|
|
113
|
+
outcome: "saved",
|
|
114
|
+
viewingSaved: false,
|
|
115
|
+
durableSaved: false,
|
|
116
|
+
withheld: [],
|
|
117
|
+
durableChars: null,
|
|
118
|
+
errors: []
|
|
119
|
+
};
|
|
120
|
+
if (next == null || typeof next !== "object") return { ...result, outcome: "ignored" };
|
|
121
|
+
const bag = next;
|
|
122
|
+
const durable = service.durable || null;
|
|
123
|
+
let forFile = bag;
|
|
124
|
+
if (durable && isBag(bag)) {
|
|
125
|
+
let trimmed = null;
|
|
126
|
+
const withheld = /* @__PURE__ */ new Set();
|
|
127
|
+
for (const key of Object.keys(bag)) {
|
|
128
|
+
const policy = policyFor(service, key);
|
|
129
|
+
const keep = policy.lifetime !== "viewing" && (policy.durableWhen === void 0 || preferenceOn(prefs, policy.durableWhen));
|
|
130
|
+
if (keep) continue;
|
|
131
|
+
trimmed ??= { ...bag };
|
|
132
|
+
delete trimmed[key];
|
|
133
|
+
if (policy.lifetime !== "viewing") withheld.add(key);
|
|
134
|
+
}
|
|
135
|
+
if (trimmed) forFile = trimmed;
|
|
136
|
+
const declared = Object.keys(service.policy || {});
|
|
137
|
+
result.withheld = [...declared.filter((k) => withheld.has(k)), ...[...withheld].filter((k) => !declared.includes(k))];
|
|
138
|
+
}
|
|
139
|
+
if (durable && service.durableMaxChars) {
|
|
140
|
+
try {
|
|
141
|
+
result.durableChars = JSON.stringify(forFile).length;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
result.errors.push({ store: "durable", error });
|
|
144
|
+
return { ...result, outcome: "ignored" };
|
|
145
|
+
}
|
|
146
|
+
if (result.durableChars > service.durableMaxChars) return { ...result, outcome: "too-large" };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
service.viewing.save(bag);
|
|
150
|
+
result.viewingSaved = true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
result.errors.push({ store: "viewing", error });
|
|
153
|
+
}
|
|
154
|
+
if (durable) {
|
|
155
|
+
try {
|
|
156
|
+
durable.save(forFile);
|
|
157
|
+
result.durableSaved = true;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
result.errors.push({ store: "durable", error });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
CHART_OWNED_KEY_POLICY,
|
|
166
|
+
commitViewState,
|
|
167
|
+
noopViewStateService,
|
|
168
|
+
resolveViewState,
|
|
169
|
+
viewStateAfterSwap,
|
|
170
|
+
viewStateIsDurable
|
|
171
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.83",
|
|
4
4
|
"description": "Run a BIC-generated D3 chart in any web host: compiles the generated render() function, applies the shared option defaults, resolves mark clicks (through tooltip overlays), owns the selection affordance, and translates row indices between cross-filtered charts. The same contract the BIC Power BI visual implements, minus Power BI. React bindings at @bicharts/chart-host/react.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
"import": "./dist/card.mjs",
|
|
50
50
|
"default": "./dist/card.mjs"
|
|
51
51
|
},
|
|
52
|
+
"./view-state": {
|
|
53
|
+
"types": "./dist/types/viewState.d.ts",
|
|
54
|
+
"import": "./dist/view-state.mjs",
|
|
55
|
+
"default": "./dist/view-state.mjs"
|
|
56
|
+
},
|
|
52
57
|
"./adapters": {
|
|
53
58
|
"types": "./dist/types/adapters/index.d.ts",
|
|
54
59
|
"import": "./dist/adapters.mjs",
|