Refuse a Data Library entry past the 64-entry cap before it registers

Add registered and switched on an entry before the selection was cut to
its first 64, so a 65th was dropped from the selection but stayed on:
Browse showed it as not added, share links left it out and Remove all
missed it. Start-up registered a share's entries the same way past the
cap. Add now refuses an entry a full library cannot keep, and start-up
registers only what the selection keeps.
This commit is contained in:
Sameh Khamis committed 2026-11-12 15:53:29 -08:00
1 parent e25cce874d
commit f33a6ada52
3 files changed
+72 -6

No files matched your search

+12 -5
View File
@@ -9,6 +9,7 @@
import { LIBRARY_ENTRIES, LIBRARY_LEDGER } from './catalog.js';
import { libraryEntryIdOf, libraryLayerId } from './manifest.js';
import {
MAX_SELECTED,
decodeLibraryParam,
encodeLibraryParam,
normalizeLibrarySelection,
@@ -270,14 +271,17 @@ export function createDataLibrary({
// link, it alone decides what is on, as it does for core layers, so a
// share with every library entry off switches the stored ones off.
// Added entries register even while off, so their rows show.
for (const id of [...stored.added, ...(linked || [])]) {
// Only what the selection keeps registers: past its cap an entry would
// be a row the library no longer knows.
const next = normalizeLibrarySelection(
{ added: [...stored.added, ...(linked || [])], on: linked ?? stored.on },
switchable,
);
for (const id of next.added) {
const entry = byId.get(id);
if (entry && addable(id)) register(entry);
}
applySelection(
{ added: [...stored.added, ...(linked || [])], on: linked ?? stored.on },
linked ? 'share-restore' : 'local-restore',
);
applySelection(next, linked ? 'share-restore' : 'local-restore');
}
/**
@@ -287,6 +291,9 @@ export function createDataLibrary({
*/
async function add(id) {
if (destroyed || !addable(id) || pending.has(id)) return false;
// A full library refuses the entry before its layer exists.
if (!selection.added.includes(id) && selection.added.length >= MAX_SELECTED)
return false;
const entry = byId.get(id);
const work = (async () => {
if (!register(entry)) return false;
+58
View File
@@ -345,6 +345,64 @@ test('a share opened while an earlier one is still switching on wins', async (t)
library.destroy();
});
test('a full library refuses another entry before its layer exists', async (t) => {
const many = validateLibraryCatalog(
Array.from({ length: 66 }, (_, index) => tiles(`entry-${index}`)),
).entries;
const manyLedger = many.map(({ id }, index) => [
id,
index.toString(36).padStart(2, '0'),
]);
const availability = {
async getAvailability() {
return {
server: true,
availability: new Map(
many.map(({ id }) => [id, { available: true, reason: null }]),
),
};
},
tileTemplate: (entry) => entry.source.url,
};
const full = JSON.stringify({
added: many.slice(0, 64).map(({ id }) => id),
on: [],
});
const manager = sealedManager(t);
const library = createDataLibrary({
entries: many,
ledger: manyLedger,
dataManager: manager,
services: {},
storage: memoryStorage(full),
shareLink: fakeShareLink(null),
source: availability,
});
await library.start();
assert.equal(manager.layers.size, 64);
assert.equal(await library.add('entry-64'), false);
assert.equal(manager.layers.size, 64, 'no layer outside the selection');
// A share into a full library adds no row the library cannot keep.
const shared = sealedManager(t);
const opened = createDataLibrary({
entries: many,
ledger: manyLedger,
dataManager: shared,
services: {},
storage: memoryStorage(full),
shareLink: fakeShareLink([manyLedger[65][1]]),
source: availability,
});
await opened.start();
assert.equal(shared.layers.has('lib-entry-65'), false);
assert.equal(
opened.list().filter((row) => row.added).length,
shared.layers.size,
);
library.destroy();
opened.destroy();
});
test('zoom flies to an added layer through the caller, reset removes everything', async (t) => {
const manager = sealedManager(t);
const storage = memoryStorage();
+2 -1
View File
@@ -12,7 +12,8 @@ import { LIBRARY_ID_PATTERN } from './manifest.js';
export const LIBRARY_STORAGE_KEY = 'gev:library:v1';
export const LIBRARY_SHARE_PARAM = 'lib';
export const LIBRARY_TOKEN_PATTERN = /^[0-9a-z]{2}$/;
const MAX_SELECTED = 64;
/** The most entries one library (and one share link) holds. */
export const MAX_SELECTED = 64;
const MAX_PARAM_CHARS = 3 * MAX_SELECTED;
/**