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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | 3x 420x 420x 420x 420x 420x 420x 420x 420x 420x 2243x 1886x 1886x 40x 43x 34x 82x 59x 140x 140x 140x 140x 140x 280x 140x 262x 869x 2x 744x 54x 54x 54x 54x 54x 54x 43x 38x 1x 1x 1x 37x 54x 12x 10x 10x 10x 10x 10x 49x 33x 37x 37x 9x 9x 83x 83x 83x 60x 4x 82x 83x 83x 83x 82x 4x 6x 153x | import { reportLoadFailure } from "./reporting.js";
import { sourceName } from "./media-catalogue.js";
const DriveNames = ["drive 0", "drive 1"];
/** One place media goes: a drive or the deck. What it holds is read from the machine itself. */
class Slot {
constructor(slots, kind, index) {
this.slots = slots;
this.kind = kind;
this.index = index;
this.name = kind === "tape" ? "the deck" : DriveNames[index];
/** How the media is known: a URL reference, or a session: reference for a file opened here. */
this.ref = undefined;
this.inUrl = false;
/** The descriptor of the load in flight, and of the load that last failed, for the window. */
this.busy = null;
this.failed = null;
this.claim = null;
}
get isDeck() {
return this.kind === "tape";
}
get media() {
const { processor } = this.slots;
return this.isDeck ? processor.tapeInterface.tape : processor.fdc?.drives[this.index]?.disc;
}
/** The URL parameters that name this slot's media, cleared when it is unnamed or empty. */
urlParams() {
return this.urlParamsFor(this.inUrl ? this.ref : undefined);
}
/** The URL parameters that would name `ref` as this slot's media. */
urlParamsFor(ref) {
if (this.isDeck) return { tape: ref };
// The URL has always called the drives disc1 and disc2, and a bare disc means disc1.
return this.index === 0 ? { disc: undefined, disc1: ref } : { disc2: ref };
}
/** What the URL names for this slot now, reading a bare disc as disc1. */
namedInUrl(params) {
if (this.isDeck) return params.tape;
return this.index === 0 ? (params.disc1 ?? params.disc) : params.disc2;
}
}
/**
* The three slots media goes in: drive 0, drive 1 and the cassette deck. Each slot is the one
* owner of what it holds, how it is known, the load in flight and the last failure, so every
* route that changes one (the list, a dropped file, the URL at startup, the desktop menu, a
* restored state) comes through here and the rest of the page only listens. A load asked for
* later always wins over one still in flight, whichever finishes first. Raises "changed" with
* the slot whenever any of that moves.
*/
export class MediaSlots extends EventTarget {
/**
* @param {object} deps
* @param {object} deps.loader fetches a reference into a disc or a tape (loadDiscImage, loadTapeImage)
* @param {import("./drives.js").Drives} deps.drives
*/
constructor({ loader, drives, processor, urlState }) {
super();
this.loader = loader;
this.drives = drives;
this.processor = processor;
this.urlState = urlState;
this.driveSlots = [0, 1].map((index) => new Slot(this, "disc", index));
this.deck = new Slot(this, "tape", 0);
}
get all() {
return [...this.driveSlots, this.deck];
}
drive(index) {
return this.driveSlots[index];
}
/** The slot the window's targets name: a drive index, or "tape". */
slotFor(target) {
return target === "tape" ? this.deck : this.driveSlots[target];
}
/** What a slot holds, by the reference it is known by. */
holding(ref) {
return this.all.find((slot) => slot.media && slot.ref === ref) ?? null;
}
/**
* Loads a descriptor's media into a slot, reporting a failure on the slot and as a toast.
* The default fetch resolves the descriptor's reference; `fetch` can make the media some
* other way (a disc created on Google Drive, say) and name the reference it ends up with.
*
* @param {object} [options]
* @param {Function} [options.fetch] async, returning `{ media, ref }`; a failure of it is not kept for a retry
* @param {boolean} [options.inUrl] whether the URL names what was loaded; a file opened this session never is
* @returns {Promise<"loaded"|"overtaken"|"failed"|"nothing">}
*/
async load(slot, descriptor, { fetch, inUrl } = {}) {
const claim = (slot.claim = {});
slot.busy = descriptor;
slot.failed = null;
this.changed(slot);
try {
const { media, ref = descriptor.ref } = await (fetch ?? (() => this.fetchDefault(slot, descriptor)))();
if (slot.claim !== claim) return "overtaken";
if (!media) {
slot.busy = null;
this.changed(slot);
return "nothing";
}
this.install(slot, media, ref, inUrl ?? descriptor.source !== "session");
return "loaded";
} catch (error) {
if (slot.claim !== claim) return "overtaken";
slot.busy = null;
if (!fetch) slot.failed = { descriptor, error };
reportLoadFailure(`${descriptor.title} from ${sourceName(descriptor.source)}`, error);
this.changed(slot);
return "failed";
}
}
async fetchDefault(slot, descriptor) {
const media = slot.isDeck
? await this.loader.loadTapeImage(descriptor.ref)
: await this.loader.loadDiscImage(descriptor.ref, this.drives.layoutForDrive(slot.index));
return { media };
}
/** Media that is already to hand goes straight in, overtaking any load still in flight. */
put(slot, media, ref, { inUrl = true } = {}) {
slot.claim = {};
this.install(slot, media, ref, inUrl);
}
eject(slot) {
slot.claim = {};
this.install(slot, undefined, undefined, true);
}
install(slot, media, ref, inUrl) {
slot.busy = null;
slot.failed = null;
if (slot.isDeck) this.processor.tapeInterface.setTape(media);
else if (media) this.drives.putDiscIn(slot.index, media);
else this.drives.eject(slot.index);
slot.ref = media ? ref : undefined;
slot.inUrl = inUrl;
// A URL that already says as much is left alone, so a load it asked for makes no history.
const named = inUrl ? slot.ref : undefined;
if (slot.namedInUrl(this.urlState.params) !== named) this.urlState.set(slot.urlParams());
this.changed(slot);
}
/** A restored state has put the machine's side of every slot back, behind the slots' backs. */
restored() {
for (const slot of this.driveSlots) this.drives.followRestoredPitch(slot.index);
for (const slot of this.all) this.changed(slot);
}
changed(slot) {
this.dispatchEvent(new CustomEvent("changed", { detail: { slot } }));
}
}
|