@aics/vole-app 2.15.0 → 3.1.1
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/es/aics-image-viewer/components/App/ChannelUpdater.js +21 -3
- package/es/aics-image-viewer/components/App/index.js +156 -199
- package/es/aics-image-viewer/components/CellViewerCanvasWrapper/index.js +10 -9
- package/es/aics-image-viewer/components/ChannelsWidget.js +35 -22
- package/es/aics-image-viewer/components/ChannelsWidgetRow/index.js +27 -71
- package/es/aics-image-viewer/components/ChannelsWidgetRow/styles.css +0 -6
- package/es/aics-image-viewer/components/ColorPicker.js +1 -1
- package/es/aics-image-viewer/components/ControlPanel/index.js +5 -4
- package/es/aics-image-viewer/components/CustomizeWidget.js +12 -8
- package/es/aics-image-viewer/components/ErrorAlert/index.js +4 -4
- package/es/aics-image-viewer/components/GlobalVolumeControls.js +12 -10
- package/es/aics-image-viewer/components/StyleProvider/index.js +4 -2
- package/es/aics-image-viewer/components/TfEditor/index.js +304 -154
- package/es/aics-image-viewer/components/TfEditor/styles.css +53 -31
- package/es/aics-image-viewer/components/Toolbar/DownloadButton.js +1 -1
- package/es/aics-image-viewer/components/Toolbar/index.js +18 -16
- package/es/aics-image-viewer/components/shared/SharedCheckBox.js +1 -1
- package/es/aics-image-viewer/components/shared/SliderRow/styles.css +1 -0
- package/es/aics-image-viewer/components/shared/SmarterSlider.js +1 -1
- package/es/aics-image-viewer/components/shared/ViewerIcon.js +1 -1
- package/es/aics-image-viewer/components/useVolume.js +59 -58
- package/es/aics-image-viewer/shared/constants.js +2 -2
- package/es/aics-image-viewer/shared/utils/controlPointsToLut.js +57 -24
- package/es/aics-image-viewer/shared/utils/sceneStore.js +5 -3
- package/es/aics-image-viewer/shared/utils/storage.js +238 -0
- package/es/aics-image-viewer/shared/utils/test/storage.test.js +264 -0
- package/es/aics-image-viewer/shared/utils/test/urlParsing.test.js +240 -108
- package/es/aics-image-viewer/shared/utils/test/viewerChannelSettings.test.js +1 -1
- package/es/aics-image-viewer/shared/utils/urlParsing.js +278 -153
- package/es/aics-image-viewer/shared/utils/viewerState.js +4 -24
- package/es/aics-image-viewer/state/reset.js +111 -0
- package/es/aics-image-viewer/state/store.js +90 -0
- package/es/aics-image-viewer/state/subscribers.js +153 -0
- package/es/aics-image-viewer/state/test/reset.test.js +247 -0
- package/es/aics-image-viewer/state/util.js +71 -0
- package/es/index.js +3 -3
- package/package.json +18 -11
- package/type-declarations/aics-image-viewer/components/App/ChannelUpdater.d.ts +2 -4
- package/type-declarations/aics-image-viewer/components/App/types.d.ts +3 -3
- package/type-declarations/aics-image-viewer/components/AxisClipSliders/index.d.ts +6 -6
- package/type-declarations/aics-image-viewer/components/CellViewerCanvasWrapper/index.d.ts +5 -14
- package/type-declarations/aics-image-viewer/components/ChannelsWidget.d.ts +3 -6
- package/type-declarations/aics-image-viewer/components/ChannelsWidgetRow/index.d.ts +3 -6
- package/type-declarations/aics-image-viewer/components/ColorPicker.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/ControlPanel/index.d.ts +6 -7
- package/type-declarations/aics-image-viewer/components/CustomizeWidget.d.ts +2 -8
- package/type-declarations/aics-image-viewer/components/ErrorAlert/index.d.ts +4 -0
- package/type-declarations/aics-image-viewer/components/GlobalVolumeControls.d.ts +2 -9
- package/type-declarations/aics-image-viewer/components/MetadataViewer/index.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/StyleProvider/index.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/TfEditor/index.d.ts +10 -3
- package/type-declarations/aics-image-viewer/components/Toolbar/index.d.ts +3 -12
- package/type-declarations/aics-image-viewer/components/shared/SliderRow/index.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/shared/SmarterSlider.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/shared/ViewerIcon.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/useVolume.d.ts +7 -4
- package/type-declarations/aics-image-viewer/shared/constants.d.ts +5 -5
- package/type-declarations/aics-image-viewer/shared/types.d.ts +1 -1
- package/type-declarations/aics-image-viewer/shared/utils/controlPointsToLut.d.ts +9 -5
- package/type-declarations/aics-image-viewer/shared/utils/firebase/index.d.ts +1 -1
- package/type-declarations/aics-image-viewer/shared/utils/hooks.d.ts +1 -1
- package/type-declarations/aics-image-viewer/shared/utils/playControls.d.ts +1 -1
- package/type-declarations/aics-image-viewer/shared/utils/sceneStore.d.ts +8 -4
- package/type-declarations/aics-image-viewer/shared/utils/storage.d.ts +15 -0
- package/type-declarations/aics-image-viewer/shared/utils/urlParsing.d.ts +94 -31
- package/type-declarations/aics-image-viewer/shared/utils/viewerChannelSettings.d.ts +39 -1
- package/type-declarations/aics-image-viewer/shared/utils/viewerState.d.ts +3 -6
- package/type-declarations/aics-image-viewer/state/reset.d.ts +29 -0
- package/type-declarations/aics-image-viewer/state/store.d.ts +30 -0
- package/type-declarations/aics-image-viewer/state/subscribers.d.ts +4 -0
- package/type-declarations/aics-image-viewer/state/types.d.ts +46 -0
- package/type-declarations/aics-image-viewer/state/util.d.ts +8 -0
- package/type-declarations/index.d.ts +4 -4
- package/es/aics-image-viewer/components/ViewerStateProvider/ResetStateProvider.js +0 -163
- package/es/aics-image-viewer/components/ViewerStateProvider/index.js +0 -259
- package/type-declarations/aics-image-viewer/components/ViewerStateProvider/ResetStateProvider.d.ts +0 -34
- package/type-declarations/aics-image-viewer/components/ViewerStateProvider/index.d.ts +0 -33
- package/type-declarations/aics-image-viewer/components/ViewerStateProvider/types.d.ts +0 -107
- /package/es/aics-image-viewer/{components/ViewerStateProvider → state}/types.js +0 -0
|
@@ -1,20 +1,30 @@
|
|
|
1
|
+
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
1
2
|
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
2
3
|
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
4
|
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
4
5
|
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
6
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
7
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
8
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
9
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
|
|
10
|
+
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
5
11
|
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
|
|
6
12
|
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
7
13
|
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
8
14
|
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
|
|
9
15
|
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
|
|
10
16
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
11
|
-
import { Lut
|
|
17
|
+
import { Lut } from "@aics/vole-core";
|
|
12
18
|
import { LUT_MAX_PERCENTILE, LUT_MIN_PERCENTILE, TFEDITOR_DEFAULT_COLOR, TFEDITOR_MAX_BIN } from "../constants";
|
|
13
19
|
import { findFirstChannelMatch } from "./viewerChannelSettings";
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object[]} controlPoints - array of `{x:number, opacity:number,
|
|
23
|
+
* color:string}`, where `x` is a histogram bin index.
|
|
24
|
+
* @returns {Uint8Array} array of length 256*4 representing the rgba values of
|
|
25
|
+
* the gradient
|
|
26
|
+
*/
|
|
27
|
+
export function binIndexedControlPointsToLut(controlPoints) {
|
|
18
28
|
var lut = new Lut().createFromControlPoints(controlPoints);
|
|
19
29
|
return lut;
|
|
20
30
|
}
|
|
@@ -77,19 +87,25 @@ function parseLutValue(value, histogram) {
|
|
|
77
87
|
* ```
|
|
78
88
|
*/
|
|
79
89
|
export function parseLutFromSettings(histogram, initSettings) {
|
|
90
|
+
var _initSettings$intensi, _initSettings$intensi2;
|
|
80
91
|
// TODO: Consider minimizing the types/classes this function is interacting
|
|
81
92
|
// with, since it is only using `initSettings.lut` and the returned Lut is a
|
|
82
93
|
// wrapper around the control point array, e.g.
|
|
83
94
|
// `parseControlPointsFromLutParam(histogram: Histogram, lutParam: [string,
|
|
84
95
|
// string] | undefined): ControlPoint[] | undefined`
|
|
85
|
-
|
|
96
|
+
|
|
97
|
+
// There are two possible locations for the LUT settings, due to legacy
|
|
98
|
+
// reasons. `initSettings.lut` is deprecated in favor of
|
|
99
|
+
// `initSettings.intensity.lut`.
|
|
100
|
+
var settingsLut = (_initSettings$intensi = (_initSettings$intensi2 = initSettings.intensity) === null || _initSettings$intensi2 === void 0 ? void 0 : _initSettings$intensi2.lut) !== null && _initSettings$intensi !== void 0 ? _initSettings$intensi : initSettings.lut;
|
|
101
|
+
if (settingsLut === undefined || settingsLut.length !== 2) {
|
|
86
102
|
return undefined;
|
|
87
103
|
}
|
|
88
104
|
var lutValues;
|
|
89
|
-
if (
|
|
105
|
+
if (settingsLut[0] === "autoij" || settingsLut[1] === "autoij") {
|
|
90
106
|
lutValues = histogram.findAutoIJBins();
|
|
91
107
|
} else {
|
|
92
|
-
lutValues = [parseLutValue(
|
|
108
|
+
lutValues = [parseLutValue(settingsLut[0], histogram), parseLutValue(settingsLut[1], histogram)];
|
|
93
109
|
}
|
|
94
110
|
if (!Number.isFinite(lutValues[0]) || !Number.isFinite(lutValues[1])) {
|
|
95
111
|
return undefined;
|
|
@@ -128,7 +144,7 @@ export function parseLutFromSettings(histogram, initSettings) {
|
|
|
128
144
|
* to the volume; otherwise, the ramp will be applied.
|
|
129
145
|
*/
|
|
130
146
|
export function initializeLut(aimg, channelIndex, channelSettings) {
|
|
131
|
-
var _initSettings$
|
|
147
|
+
var _initSettings$intensi3, _initSettings$intensi4;
|
|
132
148
|
var histogram = aimg.getHistogram(channelIndex);
|
|
133
149
|
var defaultLut = getDefaultLut(histogram);
|
|
134
150
|
var ramp = [];
|
|
@@ -137,15 +153,42 @@ export function initializeLut(aimg, channelIndex, channelSettings) {
|
|
|
137
153
|
var name = aimg.channelNames[channelIndex];
|
|
138
154
|
var initSettings = channelSettings && findFirstChannelMatch(name, channelIndex, channelSettings);
|
|
139
155
|
|
|
140
|
-
// Attempt to load a LUT from the settings, which will be used
|
|
141
|
-
|
|
156
|
+
// Attempt to load a LUT from the settings, which will be used as a fallback
|
|
157
|
+
// to initialize the control points and ramp.
|
|
158
|
+
if (initSettings) {
|
|
142
159
|
var _parseLutFromSettings;
|
|
143
160
|
lut = (_parseLutFromSettings = parseLutFromSettings(histogram, initSettings)) !== null && _parseLutFromSettings !== void 0 ? _parseLutFromSettings : defaultLut;
|
|
144
161
|
}
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
|
|
163
|
+
// Use raw intensity values for control points or ramp if provided in the
|
|
164
|
+
// settings. Otherwise, get default values from the LUT by remapping
|
|
165
|
+
// from histogram bin indices.
|
|
166
|
+
if (initSettings !== null && initSettings !== void 0 && (_initSettings$intensi3 = initSettings.intensity) !== null && _initSettings$intensi3 !== void 0 && _initSettings$intensi3.controlPoints) {
|
|
167
|
+
// Raw intensity values can be used directly.
|
|
168
|
+
controlPoints = initSettings.intensity.controlPoints;
|
|
169
|
+
} else {
|
|
170
|
+
var _initSettings$control;
|
|
171
|
+
// No provided value; use histogram to convert from bin index to raw
|
|
172
|
+
// intensity values.
|
|
173
|
+
var binIndexedControlPoints = (_initSettings$control = initSettings === null || initSettings === void 0 ? void 0 : initSettings.controlPoints) !== null && _initSettings$control !== void 0 ? _initSettings$control : _toConsumableArray(lut.controlPoints);
|
|
174
|
+
controlPoints = binIndexedControlPoints.map(function (cp) {
|
|
175
|
+
return _objectSpread(_objectSpread({}, cp), {}, {
|
|
176
|
+
x: histogram.getValueFromBinIndex(cp.x)
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Initialize the ramp
|
|
182
|
+
if (initSettings !== null && initSettings !== void 0 && (_initSettings$intensi4 = initSettings.intensity) !== null && _initSettings$intensi4 !== void 0 && _initSettings$intensi4.ramp) {
|
|
183
|
+
ramp = rampToControlPoints(initSettings.intensity.ramp);
|
|
184
|
+
} else {
|
|
185
|
+
var binIndexedRamp = initSettings !== null && initSettings !== void 0 && initSettings.ramp ? rampToControlPoints(initSettings.ramp) : _toConsumableArray(lut.controlPoints);
|
|
186
|
+
ramp = binIndexedRamp.map(function (cp) {
|
|
187
|
+
return _objectSpread(_objectSpread({}, cp), {}, {
|
|
188
|
+
x: histogram.getValueFromBinIndex(cp.x)
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
149
192
|
|
|
150
193
|
// Apply whatever lut is currently visible
|
|
151
194
|
var visibleLut;
|
|
@@ -198,14 +241,4 @@ export function rampToControlPoints(_ref) {
|
|
|
198
241
|
opacity: 1,
|
|
199
242
|
color: TFEDITOR_DEFAULT_COLOR
|
|
200
243
|
}];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Remaps an array of control points from an old range (as a 2-tuple) to a new one (extracted from a `Channel`) */
|
|
204
|
-
export function remapControlPointsForChannel(controlPoints, oldRange, _ref3) {
|
|
205
|
-
var rawMin = _ref3.rawMin,
|
|
206
|
-
rawMax = _ref3.rawMax;
|
|
207
|
-
if (oldRange === undefined) {
|
|
208
|
-
return controlPoints;
|
|
209
|
-
}
|
|
210
|
-
return remapControlPoints(controlPoints, oldRange[0], oldRange[1], rawMin, rawMax);
|
|
211
244
|
}
|
|
@@ -68,7 +68,8 @@ var SceneStore = /*#__PURE__*/function () {
|
|
|
68
68
|
}, {
|
|
69
69
|
key: "loadScene",
|
|
70
70
|
value: function () {
|
|
71
|
-
var _loadScene = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(scene, image, loadSpec,
|
|
71
|
+
var _loadScene = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(scene, image, loadSpec, options) {
|
|
72
|
+
var _options$onCreateScen;
|
|
72
73
|
var loader, spec;
|
|
73
74
|
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
|
|
74
75
|
while (1) switch (_context2.prev = _context2.next) {
|
|
@@ -83,8 +84,9 @@ var SceneStore = /*#__PURE__*/function () {
|
|
|
83
84
|
return loader.createImageInfo(spec);
|
|
84
85
|
case 7:
|
|
85
86
|
image.imageInfo.imageInfo = _context2.sent.imageInfo;
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
options === null || options === void 0 || (_options$onCreateScen = options.onCreateScene) === null || _options$onCreateScen === void 0 || _options$onCreateScen.call(options, image, scene);
|
|
88
|
+
loader.loadVolumeData(image, spec, options === null || options === void 0 ? void 0 : options.onChannelLoaded);
|
|
89
|
+
case 10:
|
|
88
90
|
case "end":
|
|
89
91
|
return _context2.stop();
|
|
90
92
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
2
|
+
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
|
|
3
|
+
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
4
|
+
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
|
|
5
|
+
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
|
|
6
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
7
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
|
|
8
|
+
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
9
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
10
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
11
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
12
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
13
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
|
|
14
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
15
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
16
|
+
import { mapKeys, mapValues } from "lodash";
|
|
17
|
+
var QUEUE_KEY = "GLOBAL_ENTRY_QUEUE";
|
|
18
|
+
var StorageEntryType = /*#__PURE__*/function (StorageEntryType) {
|
|
19
|
+
StorageEntryType["Scenes"] = "scenes";
|
|
20
|
+
StorageEntryType["Meta"] = "meta";
|
|
21
|
+
return StorageEntryType;
|
|
22
|
+
}(StorageEntryType || {});
|
|
23
|
+
var MAX_ENTRIES = {
|
|
24
|
+
scenes: 25,
|
|
25
|
+
meta: 1000
|
|
26
|
+
};
|
|
27
|
+
var sanitizeStorageKey = function sanitizeStorageKey(key) {
|
|
28
|
+
return key.includes(",") ? encodeURIComponent(key) : key;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `window.localStorage.setItem`, but returns `false` on quota exceeded instead of erroring */
|
|
32
|
+
function safeSetItem(key, value) {
|
|
33
|
+
try {
|
|
34
|
+
window.localStorage.setItem(key, value);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// `setItem` throws a `QuotaExceededError` when the browser won't let us put any more data in local storage
|
|
38
|
+
if (e instanceof DOMException && e.name === "QuotaExceededError") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrapper for `window.localStorage.setItem` that accepts a `queue` of evictable storage keys, and attempts to safely
|
|
47
|
+
* handle exceeding the storage quota by removing items off the back of the `queue` until space is available.
|
|
48
|
+
*
|
|
49
|
+
* Also, unlike `setItem`, this function can accept `value`s of type `string[]`. This allows `queue` to also be passed
|
|
50
|
+
* into `value`, so that it can be written to storage *after* picking up any evictions that are made during the write.
|
|
51
|
+
*
|
|
52
|
+
* Returns `false` if `value` could not fit into storage even after removing everything from `queue`, `true` otherwise.
|
|
53
|
+
*/
|
|
54
|
+
function setStorageItem(key, value, queue) {
|
|
55
|
+
var removedItems = [];
|
|
56
|
+
while (!safeSetItem(key, Array.isArray(value) ? value.join(",") : value)) {
|
|
57
|
+
// can't fit `value` in the store; remove something off the back of `queue` and try again
|
|
58
|
+
var evictKey = queue.shift();
|
|
59
|
+
if (evictKey === undefined) {
|
|
60
|
+
// nothing left to remove means `value` is larger than the entire store
|
|
61
|
+
// give up on inserting it and try to put everything we removed back
|
|
62
|
+
var _iterator = _createForOfIteratorHelper(removedItems),
|
|
63
|
+
_step;
|
|
64
|
+
try {
|
|
65
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
66
|
+
var _step$value = _slicedToArray(_step.value, 2),
|
|
67
|
+
_key = _step$value[0],
|
|
68
|
+
_value = _step$value[1];
|
|
69
|
+
if (safeSetItem(_key, _value)) {
|
|
70
|
+
queue.unshift(_key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
_iterator.e(err);
|
|
75
|
+
} finally {
|
|
76
|
+
_iterator.f();
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
var removedValue = window.localStorage.getItem(evictKey);
|
|
81
|
+
if (removedValue !== null) {
|
|
82
|
+
removedItems.push([evictKey, removedValue]);
|
|
83
|
+
window.localStorage.removeItem(evictKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
var getStorageQueue = function getStorageQueue() {
|
|
89
|
+
var _window$localStorage$, _window$localStorage$2;
|
|
90
|
+
return (_window$localStorage$ = (_window$localStorage$2 = window.localStorage.getItem(QUEUE_KEY)) === null || _window$localStorage$2 === void 0 ? void 0 : _window$localStorage$2.split(",")) !== null && _window$localStorage$ !== void 0 ? _window$localStorage$ : [];
|
|
91
|
+
};
|
|
92
|
+
/** If this function returns `false`, something serious has gone wrong and using local storage is likely impossible. */
|
|
93
|
+
var setStorageQueue = function setStorageQueue(queue) {
|
|
94
|
+
return setStorageItem(QUEUE_KEY, queue, queue);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Writes a bundle of `entries` to local storage, all with an optional `entryType` for categorizing the data.
|
|
99
|
+
*
|
|
100
|
+
* Entries written with this function are tracked like a least-recently-used cache, and evicted when either:
|
|
101
|
+
* - the size of local storage exceeds the browser's quota
|
|
102
|
+
* - the number of entries of type `entryType` exceeds the maximum for that type
|
|
103
|
+
*
|
|
104
|
+
* Returns `true` if all entries fit in storage, or `false` if some did not.
|
|
105
|
+
*/
|
|
106
|
+
function writeStorage(entries, entryType) {
|
|
107
|
+
var prevQueue = getStorageQueue();
|
|
108
|
+
var typePrefix = entryType ? "".concat(entryType, "@") : "";
|
|
109
|
+
var escapedEntries = mapKeys(entries, function (_, key) {
|
|
110
|
+
return sanitizeStorageKey(key);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// filter keys we're currently inserting out of the queue (to be re-inserted at the front)
|
|
114
|
+
var index = 0;
|
|
115
|
+
var thisTypeIndexes = [];
|
|
116
|
+
var queue = prevQueue.filter(function (k) {
|
|
117
|
+
if (k.startsWith(typePrefix)) {
|
|
118
|
+
if (k.slice(typePrefix.length) in escapedEntries) {
|
|
119
|
+
return false;
|
|
120
|
+
} else if (entryType !== undefined) {
|
|
121
|
+
// start saving the location of entries of this type, for enforcing the maximum entry count below
|
|
122
|
+
thisTypeIndexes.push(index);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
index += 1;
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// push new keys onto the end
|
|
130
|
+
for (var _i = 0, _Object$keys = Object.keys(escapedEntries); _i < _Object$keys.length; _i++) {
|
|
131
|
+
var key = _Object$keys[_i];
|
|
132
|
+
thisTypeIndexes.push(queue.length);
|
|
133
|
+
queue.push(typePrefix + key);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// enforce the maximum number of entries for this type (e.g. no more than 25 of type "scenes")
|
|
137
|
+
if (entryType !== undefined) {
|
|
138
|
+
var evictIndexes = thisTypeIndexes.slice(0, -MAX_ENTRIES[entryType]);
|
|
139
|
+
var _iterator2 = _createForOfIteratorHelper(evictIndexes.reverse()),
|
|
140
|
+
_step2;
|
|
141
|
+
try {
|
|
142
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
143
|
+
var _index = _step2.value;
|
|
144
|
+
var _queue$splice = queue.splice(_index, 1),
|
|
145
|
+
_queue$splice2 = _slicedToArray(_queue$splice, 1),
|
|
146
|
+
evictKey = _queue$splice2[0];
|
|
147
|
+
window.localStorage.removeItem(evictKey);
|
|
148
|
+
delete escapedEntries[evictKey.slice(typePrefix.length)];
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
_iterator2.e(err);
|
|
152
|
+
} finally {
|
|
153
|
+
_iterator2.f();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// write the new entries
|
|
158
|
+
var entryList = Object.entries(escapedEntries);
|
|
159
|
+
var firstKey = entryList.length < 2 ? undefined : typePrefix + entryList[0][0];
|
|
160
|
+
var allEntriesFit = true;
|
|
161
|
+
for (var _i2 = 0, _Object$entries = Object.entries(escapedEntries); _i2 < _Object$entries.length; _i2++) {
|
|
162
|
+
var _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2),
|
|
163
|
+
_key2 = _Object$entries$_i[0],
|
|
164
|
+
value = _Object$entries$_i[1];
|
|
165
|
+
var entryFits = setStorageItem(typePrefix + _key2, value, queue);
|
|
166
|
+
allEntriesFit = allEntriesFit && entryFits;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// TODO if the queue somehow doesn't fit, it's a much more serious problem than an individual entry.
|
|
170
|
+
// Should that be communicated in the return type?
|
|
171
|
+
var queueFit = setStorageQueue(queue);
|
|
172
|
+
return allEntriesFit && queueFit && (firstKey === undefined || window.localStorage.getItem(firstKey) !== null);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Writes a bundle of metadata records keyed by image URL to local storage.
|
|
177
|
+
*
|
|
178
|
+
* Returns `true` if all records fit into local storage, or `false` if at least one did not.
|
|
179
|
+
*/
|
|
180
|
+
export function writeMetadata(meta) {
|
|
181
|
+
var stringMeta = mapValues(meta, function (metaVal) {
|
|
182
|
+
return JSON.stringify(metaVal);
|
|
183
|
+
});
|
|
184
|
+
return writeStorage(stringMeta, StorageEntryType.Meta);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Writes a scene `url` to local storage at the given `key`.
|
|
189
|
+
*
|
|
190
|
+
* Returns `false` if the entry did not fit in local storage, or `true` if it did.
|
|
191
|
+
*/
|
|
192
|
+
export function writeScenes(key, url) {
|
|
193
|
+
return writeStorage(_defineProperty({}, sanitizeStorageKey(key), url), StorageEntryType.Scenes);
|
|
194
|
+
}
|
|
195
|
+
export function readStoredMetadata(scenes) {
|
|
196
|
+
var skipCacheUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
197
|
+
var keySet = new Set();
|
|
198
|
+
var result = scenes.map(function (scene) {
|
|
199
|
+
if (Array.isArray(scene)) {
|
|
200
|
+
// can't handle multi-source scenes (yet)
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
var globalKey = "".concat(StorageEntryType.Meta, "@").concat(sanitizeStorageKey(scene));
|
|
204
|
+
var meta = window.localStorage.getItem(globalKey);
|
|
205
|
+
if (meta === null) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
keySet.add(globalKey);
|
|
209
|
+
return JSON.parse(meta);
|
|
210
|
+
});
|
|
211
|
+
if (keySet.size > 0 && !skipCacheUpdate) {
|
|
212
|
+
var prevQueue = getStorageQueue();
|
|
213
|
+
var queue = prevQueue.filter(function (key) {
|
|
214
|
+
return !keySet.has(key);
|
|
215
|
+
});
|
|
216
|
+
queue.push.apply(queue, _toConsumableArray(keySet));
|
|
217
|
+
setStorageQueue(queue);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
export function readStoredScenes(key) {
|
|
222
|
+
var skipCacheUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
223
|
+
var globalKey = "".concat(StorageEntryType.Scenes, "@").concat(sanitizeStorageKey(key));
|
|
224
|
+
var result = window.localStorage.getItem(globalKey);
|
|
225
|
+
if (result === null) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if (!skipCacheUpdate) {
|
|
229
|
+
var queue = getStorageQueue();
|
|
230
|
+
var entryIndex = queue.indexOf(globalKey);
|
|
231
|
+
if (entryIndex !== -1) {
|
|
232
|
+
queue.splice(entryIndex, 1);
|
|
233
|
+
}
|
|
234
|
+
queue.push(globalKey);
|
|
235
|
+
setStorageQueue(queue);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { readStoredMetadata, readStoredScenes, writeMetadata, writeScenes } from "../storage";
|
|
3
|
+
var LONG_STRING = "This is a very long string, long enough that it dwarfs the size of other example data used in this test. Its purpose is to test how the functions in the \"storage\" module evict data from local storage when the quota for local storage size is exceeded. The local storage quota is set to five thousand in this test environment, which is much smaller than most browsers will provide. At one thousand two hundred and thirty-four characters, this string represents a little less than a quarter of the total size of local storage in this test environment. Therefore, the tests below can reliably predict that up to three objects containing this string in one of its properties will fit in local storage, and that inserting a fourth one will trigger an eviction. They can also predict that a single object containing four or more copies of this string will not be allowed in local storage at all. This is slightly imprecise because adding items to local storage using the functions tested in this module incurs some additional storage penalties for the storage key, the stringified JSON surrounding the value, and the key and value of the least-recently-used queue. The size of local storage in this test environment is set in jest.config.js.";
|
|
4
|
+
var LARGE_ENTRY = {
|
|
5
|
+
data: LONG_STRING
|
|
6
|
+
};
|
|
7
|
+
describe("writeMetadata/readStoredMetadata", function () {
|
|
8
|
+
it("writes metadata records to local storage", function () {
|
|
9
|
+
writeMetadata({
|
|
10
|
+
foo: {
|
|
11
|
+
foo: "bar"
|
|
12
|
+
},
|
|
13
|
+
bar: {
|
|
14
|
+
bar: "foo"
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
expect(readStoredMetadata(["bar", "foo"])).toEqual([{
|
|
18
|
+
bar: "foo"
|
|
19
|
+
}, {
|
|
20
|
+
foo: "bar"
|
|
21
|
+
}]);
|
|
22
|
+
});
|
|
23
|
+
it("handles metadata keys that contain commas", function () {
|
|
24
|
+
writeMetadata({
|
|
25
|
+
"foo,bar": {
|
|
26
|
+
a: 1,
|
|
27
|
+
b: 2
|
|
28
|
+
},
|
|
29
|
+
baz: {
|
|
30
|
+
three: "eight"
|
|
31
|
+
},
|
|
32
|
+
"bar,foo": {
|
|
33
|
+
c: 3,
|
|
34
|
+
d: 4
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
expect(readStoredMetadata(["baz", "foo,bar"])).toEqual([{
|
|
38
|
+
three: "eight"
|
|
39
|
+
}, {
|
|
40
|
+
a: 1,
|
|
41
|
+
b: 2
|
|
42
|
+
}]);
|
|
43
|
+
});
|
|
44
|
+
it("overwrites duplicate entries", function () {
|
|
45
|
+
writeMetadata({
|
|
46
|
+
foo: {
|
|
47
|
+
numbers: [1, 2, 3]
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
writeMetadata({
|
|
51
|
+
foo: {
|
|
52
|
+
numbers: [4, 5, 6]
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
expect(readStoredMetadata(["foo"])).toEqual([{
|
|
56
|
+
numbers: [4, 5, 6]
|
|
57
|
+
}]);
|
|
58
|
+
});
|
|
59
|
+
it("evicts least-recently used entries when storage runs out", function () {
|
|
60
|
+
writeMetadata({
|
|
61
|
+
one: LARGE_ENTRY
|
|
62
|
+
});
|
|
63
|
+
writeMetadata({
|
|
64
|
+
two: LARGE_ENTRY,
|
|
65
|
+
three: LARGE_ENTRY
|
|
66
|
+
});
|
|
67
|
+
expect(readStoredMetadata(["one", "two", "three"])).toEqual([LARGE_ENTRY, LARGE_ENTRY, LARGE_ENTRY]);
|
|
68
|
+
writeMetadata({
|
|
69
|
+
four: LARGE_ENTRY
|
|
70
|
+
});
|
|
71
|
+
expect(readStoredMetadata(["one", "two", "three", "four"])).toEqual([undefined, LARGE_ENTRY, LARGE_ENTRY, LARGE_ENTRY]);
|
|
72
|
+
});
|
|
73
|
+
it("leaves storage unchanged when a single entry is too large to fit in storage", function () {
|
|
74
|
+
writeMetadata({
|
|
75
|
+
one: {
|
|
76
|
+
entrySize: "small"
|
|
77
|
+
},
|
|
78
|
+
two: {
|
|
79
|
+
bigness: "not_too_large"
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
writeMetadata({
|
|
83
|
+
three: {
|
|
84
|
+
favoriteNumber: 8
|
|
85
|
+
},
|
|
86
|
+
big: {
|
|
87
|
+
one: LONG_STRING,
|
|
88
|
+
two: LONG_STRING,
|
|
89
|
+
three: LONG_STRING,
|
|
90
|
+
four: LONG_STRING
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
expect(readStoredMetadata(["one", "two", "three", "big"])).toEqual([{
|
|
94
|
+
entrySize: "small"
|
|
95
|
+
}, {
|
|
96
|
+
bigness: "not_too_large"
|
|
97
|
+
}, {
|
|
98
|
+
favoriteNumber: 8
|
|
99
|
+
}, undefined]);
|
|
100
|
+
});
|
|
101
|
+
it("moves entries to the front of the queue when read", function () {
|
|
102
|
+
writeMetadata({
|
|
103
|
+
one: LARGE_ENTRY
|
|
104
|
+
});
|
|
105
|
+
writeMetadata({
|
|
106
|
+
two: LARGE_ENTRY,
|
|
107
|
+
three: LARGE_ENTRY
|
|
108
|
+
});
|
|
109
|
+
readStoredMetadata(["one"]);
|
|
110
|
+
writeMetadata({
|
|
111
|
+
four: LARGE_ENTRY,
|
|
112
|
+
five: LARGE_ENTRY
|
|
113
|
+
});
|
|
114
|
+
expect(readStoredMetadata(["one", "two", "three", "four", "five"])).toEqual([LARGE_ENTRY, undefined, undefined, LARGE_ENTRY, LARGE_ENTRY]);
|
|
115
|
+
});
|
|
116
|
+
it("moves entries to the front of the queue when overwritten", function () {
|
|
117
|
+
writeMetadata({
|
|
118
|
+
one: LARGE_ENTRY,
|
|
119
|
+
two: LARGE_ENTRY
|
|
120
|
+
});
|
|
121
|
+
writeMetadata({
|
|
122
|
+
three: LARGE_ENTRY,
|
|
123
|
+
one: LARGE_ENTRY
|
|
124
|
+
});
|
|
125
|
+
writeMetadata({
|
|
126
|
+
four: LARGE_ENTRY
|
|
127
|
+
});
|
|
128
|
+
expect(readStoredMetadata(["one", "two", "three", "four"])).toEqual([LARGE_ENTRY, undefined, LARGE_ENTRY, LARGE_ENTRY]);
|
|
129
|
+
});
|
|
130
|
+
it("returns `false` on an attempt to write more data than local storage can hold", function () {
|
|
131
|
+
var fits = writeMetadata({
|
|
132
|
+
one: LARGE_ENTRY,
|
|
133
|
+
two: LARGE_ENTRY,
|
|
134
|
+
three: LARGE_ENTRY
|
|
135
|
+
});
|
|
136
|
+
expect(fits).toBe(true);
|
|
137
|
+
var tooBig = writeMetadata({
|
|
138
|
+
big: {
|
|
139
|
+
one: LONG_STRING,
|
|
140
|
+
two: LONG_STRING,
|
|
141
|
+
four: LONG_STRING,
|
|
142
|
+
six: LONG_STRING
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
expect(tooBig).toBe(false);
|
|
146
|
+
var tooMany = writeMetadata({
|
|
147
|
+
one: LARGE_ENTRY,
|
|
148
|
+
two: LARGE_ENTRY,
|
|
149
|
+
three: LARGE_ENTRY,
|
|
150
|
+
four: LARGE_ENTRY
|
|
151
|
+
});
|
|
152
|
+
expect(tooMany).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
it("returns `undefined` on an attempt to get an array key", function () {
|
|
155
|
+
writeMetadata({
|
|
156
|
+
one: {
|
|
157
|
+
digits: 1
|
|
158
|
+
},
|
|
159
|
+
"one,two": {
|
|
160
|
+
digits: 12
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
expect(readStoredMetadata(["one", "one,two", ["one", "two"]])).toEqual([{
|
|
164
|
+
digits: 1
|
|
165
|
+
}, {
|
|
166
|
+
digits: 12
|
|
167
|
+
}, undefined]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("writeScenes/readStoredScenes", function () {
|
|
171
|
+
var EXAMPLE_URL = "https://example.com/image.zarr";
|
|
172
|
+
var EXAMPLE_URL_2 = "https://example.com/image2.zarr";
|
|
173
|
+
it("writes keyed URLs to local storage", function () {
|
|
174
|
+
writeScenes("foo", EXAMPLE_URL);
|
|
175
|
+
expect(readStoredScenes("foo")).toBe(EXAMPLE_URL);
|
|
176
|
+
});
|
|
177
|
+
it("handles keys that contain commas", function () {
|
|
178
|
+
writeScenes("foo,bar", EXAMPLE_URL);
|
|
179
|
+
expect(readStoredScenes("foo,bar")).toBe(EXAMPLE_URL);
|
|
180
|
+
});
|
|
181
|
+
it("overwrites duplicate entries", function () {
|
|
182
|
+
writeScenes("foo", EXAMPLE_URL);
|
|
183
|
+
writeScenes("foo", EXAMPLE_URL_2);
|
|
184
|
+
expect(readStoredScenes("foo")).toBe(EXAMPLE_URL_2);
|
|
185
|
+
});
|
|
186
|
+
it("evicts least-recently used entries when storage runs out", function () {
|
|
187
|
+
writeScenes("one", LONG_STRING);
|
|
188
|
+
writeScenes("two", LONG_STRING);
|
|
189
|
+
writeScenes("three", LONG_STRING);
|
|
190
|
+
writeScenes("four", LONG_STRING);
|
|
191
|
+
expect(readStoredScenes("one")).toBe(undefined);
|
|
192
|
+
expect(readStoredScenes("two")).toBe(LONG_STRING);
|
|
193
|
+
expect(readStoredScenes("three")).toBe(LONG_STRING);
|
|
194
|
+
expect(readStoredScenes("four")).toBe(LONG_STRING);
|
|
195
|
+
});
|
|
196
|
+
it("shares an eviction queue with metadata", function () {
|
|
197
|
+
writeMetadata({
|
|
198
|
+
one: LARGE_ENTRY
|
|
199
|
+
});
|
|
200
|
+
writeScenes("one", LONG_STRING);
|
|
201
|
+
writeMetadata({
|
|
202
|
+
two: LARGE_ENTRY
|
|
203
|
+
});
|
|
204
|
+
writeScenes("two", LONG_STRING);
|
|
205
|
+
expect(readStoredScenes("one")).toBe(LONG_STRING);
|
|
206
|
+
expect(readStoredMetadata(["one", "two"])).toEqual([undefined, LARGE_ENTRY]);
|
|
207
|
+
expect(readStoredScenes("two")).toBe(LONG_STRING);
|
|
208
|
+
writeMetadata({
|
|
209
|
+
three: LARGE_ENTRY
|
|
210
|
+
});
|
|
211
|
+
expect(readStoredMetadata(["one", "two", "three"])).toEqual([undefined, LARGE_ENTRY, LARGE_ENTRY]);
|
|
212
|
+
expect(readStoredScenes("one")).toBe(undefined);
|
|
213
|
+
expect(readStoredScenes("two")).toBe(LONG_STRING);
|
|
214
|
+
});
|
|
215
|
+
it("moves entries to the front of the queue when read", function () {
|
|
216
|
+
var LARGE_ENTRY = {
|
|
217
|
+
data: LONG_STRING
|
|
218
|
+
};
|
|
219
|
+
writeScenes("one", LONG_STRING);
|
|
220
|
+
writeMetadata({
|
|
221
|
+
one: LARGE_ENTRY
|
|
222
|
+
});
|
|
223
|
+
readStoredScenes("one");
|
|
224
|
+
writeMetadata({
|
|
225
|
+
two: LARGE_ENTRY,
|
|
226
|
+
three: LARGE_ENTRY
|
|
227
|
+
});
|
|
228
|
+
expect(readStoredScenes("one")).toBe(LONG_STRING);
|
|
229
|
+
expect(readStoredMetadata(["one", "two", "three"])).toEqual([undefined, LARGE_ENTRY, LARGE_ENTRY]);
|
|
230
|
+
});
|
|
231
|
+
it("moves entries to the front of the queue when overwritten", function () {
|
|
232
|
+
writeScenes("one", LONG_STRING);
|
|
233
|
+
writeMetadata({
|
|
234
|
+
two: LARGE_ENTRY
|
|
235
|
+
});
|
|
236
|
+
writeMetadata({
|
|
237
|
+
three: LARGE_ENTRY
|
|
238
|
+
});
|
|
239
|
+
writeScenes("one", LONG_STRING);
|
|
240
|
+
writeMetadata({
|
|
241
|
+
four: LARGE_ENTRY
|
|
242
|
+
});
|
|
243
|
+
expect(readStoredScenes("one")).toBe(LONG_STRING);
|
|
244
|
+
expect(readStoredMetadata(["two", "three", "four"])).toEqual([undefined, LARGE_ENTRY, LARGE_ENTRY]);
|
|
245
|
+
});
|
|
246
|
+
it("returns `false` on an attempt to write a longer scene url than local storage can hold", function () {
|
|
247
|
+
expect(writeScenes("fits", LONG_STRING)).toBe(true);
|
|
248
|
+
expect(writeScenes("does_not_fit", LONG_STRING + LONG_STRING + LONG_STRING + LONG_STRING)).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
it("enforces a maximum number of scene entries", function () {
|
|
251
|
+
var MAX_ENTRIES = 25;
|
|
252
|
+
var EXTRA_ENTRIES = 5;
|
|
253
|
+
var entriesToInsert = MAX_ENTRIES + EXTRA_ENTRIES;
|
|
254
|
+
for (var i = 0; i < entriesToInsert; i++) {
|
|
255
|
+
writeScenes("scene".concat(i), EXAMPLE_URL);
|
|
256
|
+
}
|
|
257
|
+
for (var _i = 0; _i < EXTRA_ENTRIES; _i++) {
|
|
258
|
+
expect(readStoredScenes("scene".concat(_i))).toBe(undefined);
|
|
259
|
+
}
|
|
260
|
+
for (var _i2 = EXTRA_ENTRIES; _i2 < entriesToInsert; _i2++) {
|
|
261
|
+
expect(readStoredScenes("scene".concat(_i2))).toBe(EXAMPLE_URL);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|