Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 2x 2x 2x 2x 1769x 1769x 2x 46510x 46510x 46510x 12933x 12933x 12911x 12911x 12931x 1020x 1020x 12933x 3526x 3526x 1x 1x 1x 3525x 3525x 3526x 1135x 1135x 3526x 2x 3526x 3526x 3526x 3526x 2x 12933x 12933x 1554x 1554x 12930x 12933x 2x 721x 721x 721x 721x 721x 29330x 29330x 29330x 721x 74x 721x 721x 32609x 3x 3x 3x 32609x 721x 721x | /**
* Per-file memory of the things a user typed but the URL does not carry.
*
* The view state (§5.5) is deliberately the *shareable* state: a URL names a
* genome, a region, a colour scale. Table filters are not in it, and should not
* be — a link that silently carries "only rows where completeness > 90" would
* mislead whoever opens it.
*
* But not being in the URL is not a reason to forget them. Filtering a 4,000-MAG
* table, opening a genome, and coming back to an unfiltered table means doing
* the work twice, and the second time you are retyping rather than thinking.
*
* So filters live here instead: keyed by file URI and by a caller-chosen scope,
* held for the session, restored on the way back. `sessionStorage` rather than
* `localStorage` deliberately — a filter is a train of thought, not a
* preference, and it should not still be applied next week. It also degrades to
* an in-memory map when storage is unavailable (Safari private mode, a strict
* embedding), because losing a filter must never break a view.
*/
import { embedded } from '../embed'
import { splitUri } from '../ui/labels'
const PREFIX = 'chromosomix:ui:'
/**
* The key one session's memory of one *file* hangs on.
*
* Two facts, and both of them are decisions.
*
* The **session id**, because two tabs on the same file are two independent
* workspaces: keyed on the URI alone the second would adopt the first's tracks
* and filters, which is exactly what tabs exist to prevent.
*
* The **file, not the entry inside it** — the path before `::`. An `.mcool` at
* 1 kb and the same `.mcool` at 5 kb are one file, which is the definition
* `Session.reopeningSameFile` already uses and the reason a change of resolution
* carries every field of the view state across (`Session.carryAcrossEntries`):
* names are names and coordinates are base pairs. Everything stored under this
* key is the same kind of fact — a hand-drawn mark is a contig and a base pair
* (§8.1), a ticked sequence is a name, a group is a claim about tracks, a table's
* filter is a phrase about rows — and none of them means anything different at
* another bin size.
*
* With the entry in the key all of it was *lost* on a resolution switch, and
* silently: the previous level's marks stayed in storage under its own key, so
* they came back if the reader switched back and stayed invisible if they did
* not. Tracks were the exception, and only because they are carried live across
* the reopen — which is why this surfaced as "my annotations are gone" rather
* than as everything at once.
*/
export function fileMemoryKey(sessionId: string, uri: string): string {
return `${sessionId}|${splitUri(uri).path}`
}
/** Storage that cannot throw. Falls back to a module-level map. */
const memory = new Map<string, string>()
/**
* Whether this window keeps its memory to itself, in {@link memory}: an embed
* (§21.2). Every frame a page holds on one origin shares that origin's
* `sessionStorage`, and an embed keeps no tab strip, so each starts with the
* same session id — one notebook widget's tracks came back in the next widget
* on the same map, and in the same widget's frame again when it reloaded, under
* the tracks its host put there. An embed shows what its page says, every load.
*/
function ownMemory(): boolean {
return embedded()
}
function read(key: string): string | null {
if (ownMemory()) return memory.get(key) ?? null
try {
return window.sessionStorage.getItem(key)
} catch {
return memory.get(key) ?? null
}
}
function write(key: string, value: string): void {
if (ownMemory()) {
memory.set(key, value)
return
}
try {
window.sessionStorage.setItem(key, value)
} catch {
memory.set(key, value)
}
}
/**
* Remember `value` for `scope` within `uri`.
*
* `scope` separates independent widgets on the same file — the genomes table
* and the contigs table each keep their own filters.
*/
export function remember(uri: string, scope: string, value: unknown): void {
try {
write(`${PREFIX}${uri}:${scope}`, JSON.stringify(value))
} catch {
/* an unserialisable value is a caller bug, not a reason to break a view */
}
}
/**
* Recall what {@link remember} stored, or `null`.
*
* The caller validates the shape: stored state outlives the code that wrote it
* (a reload picks up a new build against an old session), so a recalled value
* is untrusted input, exactly like the URL hash.
*/
export function recall<T>(uri: string, scope: string): T | null {
const raw = read(`${PREFIX}${uri}:${scope}`)
if (raw === null) return null
try {
return JSON.parse(raw) as T
} catch {
return null
}
}
/**
* Every value {@link remember} stored for `scope` under a key that begins
* with `prefix`, by the rest of the key — the facts kept below one file's own:
* a metagenome's genomes, each edit under its own key
* (`state/referenceEdit.ts`'s `genomeEditKey`). Unordered, and untrusted as
* {@link recall}'s are.
*/
export function recallBelow<T>(prefix: string, scope: string): Array<{ rest: string; value: T }> {
const head = `${PREFIX}${prefix}`
const tail = `:${scope}`
const keys = new Set<string>(memory.keys())
try {
for (let i = 0; !ownMemory() && i < window.sessionStorage.length; i++) {
const key = window.sessionStorage.key(i)
if (key !== null) keys.add(key)
}
} catch {
/* the map above is where everything went */
}
const out: Array<{ rest: string; value: T }> = []
for (const key of keys) {
if (!key.startsWith(head) || !key.endsWith(tail) || key.length < head.length + tail.length) continue
const rest = key.slice(head.length, key.length - tail.length)
const value = recall<T>(`${prefix}${rest}`, scope)
if (value !== null) out.push({ rest, value })
}
return out
}
|