@bicharts/chart-host 0.6.80 → 0.6.82
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-Y3CCDWVO.mjs → chunk-7HPVGRQ2.mjs} +13 -13
- package/dist/{chunk-YYJCE5TO.mjs → chunk-MP5S6MVW.mjs} +18 -18
- package/dist/{chunk-P3HUTWQB.mjs → chunk-ZRSEKV4I.mjs} +360 -368
- package/dist/index.mjs +3 -3
- 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 +1 -1
- 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/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
formatSourceDateFor,
|
|
6
6
|
isExcelDateFormat,
|
|
7
7
|
normaliseAggregation
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7HPVGRQ2.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-MP5S6MVW.mjs";
|
|
115
115
|
import {
|
|
116
116
|
_3,
|
|
117
117
|
l,
|
|
118
118
|
o
|
|
119
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-ZRSEKV4I.mjs";
|
|
120
120
|
import {
|
|
121
121
|
MIN_HIT_BAND_PX,
|
|
122
122
|
SELECTION_GLYPH_CLASS,
|
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-MP5S6MVW.mjs";
|
|
26
|
+
import "./chunk-ZRSEKV4I.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
|
@@ -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";
|
|
@@ -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
|
+
};
|