diff --git a/src/library/controller.js b/src/library/controller.js index 9e2807e4a..a2e6ecf9f 100644 --- a/src/library/controller.js +++ b/src/library/controller.js @@ -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; diff --git a/src/library/controller.test.mjs b/src/library/controller.test.mjs index 4b75e7b91..4ca490dd6 100644 --- a/src/library/controller.test.mjs +++ b/src/library/controller.test.mjs @@ -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(); diff --git a/src/library/selection.js b/src/library/selection.js index 513cedf4b..ba565b8c3 100644 --- a/src/library/selection.js +++ b/src/library/selection.js @@ -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; /**