Let a share fit a full Data Library and show entries it cannot serve

A share's entries joined the library after the stored ones, and the
selection kept its first 64, so a link opened into a full library never
showed what it shared and the next hash write dropped its lib=. Now the
oldest entries the share leaves off make room for it, and their layers
are removed with them.

Browse counted an entry as added only while its layer was registered,
but an entry this instance cannot serve (switched off by the operator,
or its server unreachable) stayed in the selection and held one of the
64 places. A library full of them looked empty and refused every Add.
Such entries now show as added, so they can be removed one by one or
with Remove all.
This commit is contained in:
Sameh Khamis committed 2026-11-12 17:49:01 -08:00
1 parent 4fe8796e89
commit 837eff7328
2 files changed
+118 -14

No files matched your search

+40 -3
View File
@@ -184,6 +184,28 @@ export function createDataLibrary({
persist();
});
/**
* The library after a share's entries join it. A share always fits: in a
* full library the oldest entries it leaves off make room for it, so a
* link opened into a full library still shows what it shares.
*/
function withShared(added, linked) {
const kept = [...new Set(added)].filter((id) => switchable.has(id));
const incoming = [...new Set(linked)].filter(
(id) => switchable.has(id) && !kept.includes(id),
);
let room = MAX_SELECTED - kept.length - incoming.length;
const result = [];
for (const id of kept) {
if (room < 0 && !linked.includes(id)) {
room += 1;
continue;
}
result.push(id);
}
return [...result, ...incoming];
}
/** Entry ids for share tokens; unknown tokens are skipped. */
const idsForTokens = (list) =>
(list || []).map((token) => tokens.idByToken.get(token)).filter(Boolean);
@@ -212,7 +234,17 @@ export function createDataLibrary({
* is never remembered as the viewer's own choice.
*/
function applySelection(next, origin) {
const before = selection.added;
selection = normalizeLibrarySelection(next, switchable);
// Entries a share made room for leave with their layers.
for (const id of before) {
if (selection.added.includes(id) || !isRegistered(id)) continue;
dataManager
.unregisterLibraryLayer(libraryLayerId(id))
.catch((error) =>
console.warn(`[Library] ${id} could not be removed:`, error),
);
}
for (const id of selection.added) {
const entry = byId.get(id);
if (!entry || !addable(id)) continue;
@@ -251,7 +283,7 @@ export function createDataLibrary({
}
const linked = idsForTokens(shareTokens);
applySelection(
{ added: [...selection.added, ...linked], on: linked },
{ added: withShared(selection.added, linked), on: linked },
'share-restore',
);
}
@@ -297,7 +329,10 @@ export function createDataLibrary({
// 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 },
{
added: linked ? withShared(stored.added, linked) : stored.added,
on: linked ?? stored.on,
},
switchable,
);
for (const id of next.added) {
@@ -423,7 +458,9 @@ export function createDataLibrary({
entry,
available: state.available,
reason: state.reason,
added: selection.added.includes(entry.id) && isRegistered(entry.id),
// An entry this instance cannot serve now stays added (and can be
// removed) without a layer: it still holds a place in the library.
added: selection.added.includes(entry.id),
on: dataManager.isEnabled?.(libraryLayerId(entry.id)) === true,
busy: pending.has(entry.id),
canZoom: typeof zoomTo === 'function' && isRegistered(entry.id),
+78 -11
View File
@@ -418,25 +418,92 @@ test('a full library refuses another entry before its layer exists', async (t) =
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({
library.destroy();
});
test('a share opened into a full library still shows what it shares', async (t) => {
const many = validateLibraryCatalog(
Array.from({ length: 67 }, (_, 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: ['entry-0'],
});
const manager = sealedManager(t);
const shareLink = {
...shareLinkDouble(),
initialLibraryTokens: [manyLedger[64][1]],
listener: null,
setLibraryShareListener(listener) {
this.listener = listener;
},
};
const library = createDataLibrary({
entries: many,
ledger: manyLedger,
dataManager: shared,
dataManager: manager,
services: {},
storage: memoryStorage(full),
shareLink: fakeShareLink([manyLedger[65][1]]),
shareLink,
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,
await library.start();
await manager.waitForLayerSettled?.('lib-entry-64');
assert.equal(manager.isEnabled('lib-entry-64'), true);
// The oldest entry the share leaves off made room.
assert.equal(manager.layers.has('lib-entry-0'), false);
assert.equal(manager.layers.size, 64);
assert.equal(shareLink.encode(), manyLedger[64][1]);
// A share navigated to in this tab makes room the same way, and the
// entry it evicts leaves with its layer.
shareLink.listener([manyLedger[65][1]]);
await manager.waitForLayerSettled?.('lib-entry-65');
await new Promise((resolve) => setImmediate(resolve));
assert.equal(manager.isEnabled('lib-entry-65'), true);
assert.equal(manager.layers.has('lib-entry-1'), false);
assert.equal(manager.layers.size, 64);
library.destroy();
});
test('entries this instance cannot serve still show as added and can be removed', async (t) => {
const manager = sealedManager(t);
const storage = memoryStorage(
JSON.stringify({ added: ['night-lights'], on: ['night-lights'] }),
);
const library = createDataLibrary({
entries,
ledger,
dataManager: manager,
services: {},
storage,
shareLink: fakeShareLink(null),
source: source({
'night-lights': { available: false, reason: 'disabled' },
}),
});
await library.start();
assert.equal(manager.layers.size, 0);
const row = library.list().find(({ entry }) => entry.id === 'night-lights');
assert.equal(row.added, true);
assert.equal(library.summary().added, 1);
assert.equal(await library.remove('night-lights'), true);
assert.deepEqual(JSON.parse(storage.store.get(LIBRARY_STORAGE_KEY)).added, []);
library.destroy();
opened.destroy();
});
test('a layer that fails to tear down stays in the library', async (t) => {