feat(cache): persistent event cache for snappier repeat visits#18
Open
feat(cache): persistent event cache for snappier repeat visits#18
Conversation
Replaces the single-screen AMB form with a 7-step guided wizard that lowers the metadata bar for non-library users. Highlights: - Step 1 "Bildungsbereich": picking Schule/Hochschule/Extra preselects the subject vocabulary and educational levels for later steps. - Step 2 URL/naddr: server-side metadata extraction (OpenGraph + AMB JSON-LD) via /api/reader prefills later steps; inspection runs automatically on type (debounced) and on paste — no manual button. Pasting an owned naddr redirects into edit mode. - Step 3 "Basic": title/description/language plus a content-fit image preview surfaced at the top of the step as a visual anchor. - Step 4 "Klassifikation": SKOS-backed pickers for resource type, educational level, and subject; vocab-slug disambiguation only shows human labels when multiple parallel pickers are active (extra BB). - Steps 5-7 unchanged (content & creators, rights, community share). Plus a terminology shift: header reads "Bildungsressource teilen" / "Share Educational Resource" — the act is making an existing resource findable, not creating it. Also adds: - Unit tests for helpers (ambJsonLdToFormData, bildungsbereich, keywordInput, metadataExtraction, resolveMetadataInput, vocabResolver) and for MetadataFetchStep. - E2E coverage for the new wizard flow.
Replace DaisyUI hover tooltips with persistent label pills next to each FAB sub-button so users (especially on touch) can see what every action does without having to hover each icon in turn. Label sits to the right of the icon, anchored to the same edge as the main "+" button. Also rename fab_create_resource to "Share Learning Content" / "Lerninhalt teilen" to match the feature rename already applied to the resource creation page title on feature/guided-amb-wizard.
Flips the default presentation view mode for calendar pages from "calendar" to "list". Updates the runtime default in CalendarView, the URL-param parser, URL builder, active-filter detection, and the initial-sync handler so the list view is the no-param default everywhere.
Adds a Relations wizard step to the AMB form with inline hasPart/isPartOf pickers (AMBResourceSearchInput, NIP-50-backed search + naddr-paste fallback). Relations are serialized as marker `a`-tags via amb-nostr-converter and re-read in edit mode via new getAMBHasPart / getAMBIsPartOf helpers. Also fixes two bugs surfaced when publishing against real data: - URL-d-tag coord truncation: the picker, the helpers reader, and the detail view all split the `kind:pubkey:dTag` coordinate on every `:`, truncating d-tags that are URLs (e.g. losing everything after `https`). Reads the d-tag directly from the source event where possible and uses a bounded split (indexOf/slice) everywhere else. Detail view now also filters to relation-marker `a`-tags only so bare `a`-tags don't render as related resources. - educationalLevel auto-preselect: picking a Bildungsbereich in step 1 silently seeded step 4 with KIM level URIs the user never chose. Removed the effect; `educationalLevelDefaults` is renamed to `educationalLevelMapping` since it's only used now for reverse inference in edit-mode prefill, not as a preselect default. Bumps amb-nostr-converter to 0.0.0-d216541 for the `relatedEvents` option that emits the marker `a`-tags.
Brings in three landed commits: - feat(amb): guided wizard for sharing educational resources - feat(calendar): default to list view - feat(amb): hasPart/isPartOf relations + URL-d-tag fixes Subsumes feature/guided-amb-wizard and feat/default-calendar-list-view, which both point at the guided-wizard commit.
Kind 30142 events now fall through to the generic /{naddr} route
instead of /discover?resource={naddr}, so clicks from the feed open
the dedicated detail view.
Rename AMBResourceForm → ResourceFormWizard and add a `variantId` prop so the same wizard serves both `amb` and `ekw` metadata profiles. Variants are gated by RESOURCE_FORM_VARIANTS env. Published events carry NIP-32 ["L", "metadata-form"] + ["l", variantId, "metadata-form"] so edit mode can reopen the correct form. - variant registry (src/lib/config/resource-form-variants.js) - resolveVariantIdFromEvent helper for edit mode - shared eventTags helpers used by create/update actions - ResourceVariantPickerModal (shown only when >1 variant enabled) - /create/resource/[variant=resourceVariant] route + redirector - FAB wiring: direct nav when single variant, picker when multi - bildungsbereich keys per variant (amb: schule/hochschule/extra, ekw: schule/konfi) - i18n, unit + E2E tests, COVERAGE.md entry
- svelte 5.49.2 → 5.55.4 (5 SSR XSS advisories)
- @sveltejs/kit 2.50.2 → 2.57.1 (kit + transitive devalue/cookie fixes)
- dompurify 3.3.1 → 3.4.1 (8 sanitization bypasses)
- applesauce-{accounts,common,core,relay,signers} 5.1.0 → 5.2.0
- applesauce-actions 5.1.0 → 5.1.1
- @sveltejs/adapter-node 5.5.2 → 5.5.4 (BODY_SIZE_LIMIT bypass)
- @tailwindcss/vite, tailwindcss 4.1.18 → 4.2.4
- daisyui 5.5.18 → 5.5.19
- vitest 4.0.18 → 4.1.5, svelte-check 4.3.6 → 4.4.6
- @playwright/test 1.58.1 → 1.59.1
- prettier 3.3.3 → 3.8.3, prettier-plugin-svelte 3.4.1 → 3.5.1
- eslint-plugin-svelte 3.14.0 → 3.17.1, lint-staged 16.2.7 → 16.4.0
- @inlang/cli 3.1.4 → 3.1.9
- nostr-tools 2.23.0 → 2.23.3
- livekit-client 2.18.1 → 2.18.5, ws 8.19.0 → 8.20.0
- svelte-maplibre 1.2.6 → 1.3.0, unpdf 1.4.0 → 1.6.0
pnpm audit: 62 → 38 vulnerabilities. Remaining advisories mostly
stem from the deprecated @inlang/paraglide-sveltekit wrapper (11
including the sole critical sha.js) and should be addressed by
migrating to @inlang/paraglide-js v2 directly.
The @inlang/paraglide-sveltekit adapter is deprecated; paraglide-js v2 now exports the Vite plugin and middleware directly. vite.config.js already uses paraglideVitePlugin from @inlang/paraglide-js, and hooks.server.js consumes the generated $lib/paraglide/server, so no source changes are needed.
The route matcher called getEnabledVariants() which reads runtimeConfig. SvelteKit runs matchers server-side during route resolution, before /api/config is fetched client-side, so the pre-config default ['amb'] was rejecting otherwise-valid URLs like /create/resource/ekw with a 404. Validate against the static ALL_VARIANTS registry in the matcher, and move enabled-list filtering into the page component: await configReady, then redirect registered-but-disabled variants to the default (preserving ?community= and ?edit=).
When configReady is already true at onMount, svelte's writable calls the subscribe callback synchronously. The callback references `unsub` before the const assignment completes, throwing "Cannot access 'unsub' before initialization". Short-circuit with get(configReady) so the subscription path only runs when we actually need to wait. Same pattern applied to both the /create/resource redirector and the /create/resource/[variant] page.
Replaces the subtle ghost button under the URL input with a full-width option card (mirrors the step-1 Bildungsbereich style), separated by an "or" divider. Clicking the card auto-advances to step 3 after 200ms, matching the step-1 radio pattern. Copy changes: - Label: "No external link?" / "Kein externer Link?" (was the jargon-heavy "Index a new Nostr-only resource") - Description: "Create the resource directly on Nostr — e.g. for files you upload." (new key, explains the two use cases) - State card (shown on back-nav and edit mode): "This resource will be created directly on Nostr …" (was "Resource without an external URL") - Cancel: "Enter a URL after all" (was "Cancel and enter a URL instead") E2E test updated: the state-card assertion was removed from the happy path (auto-advance skips past it), and the edit round-trip now asserts against the new state-card wording.
prettier 3.7+ removed the legacy getVisitorKeys export that
prettier-plugin-svelte 3.5.1 still calls from printEmbeddedLanguages,
which makes every .svelte file crash with
TypeError: getVisitorKeys is not a function or its return value is
not iterable
— breaking the pre-commit hook for the whole repo. Pinning prettier to
~3.3.3 (the last version before the crash) restores the hook while
keeping plugin-svelte 3.5.1. Bump back to the current prettier stable
once prettier-plugin-svelte ships a fix.
Three calendar fixes bundled together with shared test coverage.
- Pin persistence: CalendarMapView used to overwrite its pin list on
every geocode batch. Switched to a $state.raw coordCache Map that
merges new geocode results into existing entries, with derived pins
recomputed from the cache. Multiple concurrent IIFEs from $effect
re-runs now merge safely (last-write-wins on overlapping keys).
- Grid button: CalendarNavigation used to strip the view param from
the URL when clicking the grid button, relying on a "default is
list" branch in the loader — which meant the loader then rendered
the list view, not the grid. Now always writes the explicit view
value via updateQueryParams.
- Initial map zoom / style-load race: replaced prop-driven bounds
with a programmatic map.fitBounds(bounds, { padding: 50, maxZoom:
12, animate: false }) via bind:map + bind:loaded and a one-shot
guard flag — prevents the svelte-maplibre bidirectional state loop
where moveend handlers write bounds back to the prop.
Refactor: drop dead mapCenter/mapZoom state and collapse the
three-branch calculateMapBounds into two (empty vs. non-empty). The
single-event path now uses a degenerate bbox [[lng,lat],[lng,lat]],
which fitBounds with maxZoom: 12 centers correctly.
Tests: new E2E covering all three view-toggle buttons (locks in the
grid fix). Map-canvas rendering isn't asserted from E2E because the
mock-relay test fixtures have no location data; COVERAGE.md records
this as a known gap.
…ck derived_inert
Two related fixes surfaced while chasing an intermittent empty community
"Startseite" feed.
- Routing: +page.svelte derived `selectedContentType` fell back to the
raw `$page.url.searchParams.get('view')` when +page.js returned
`data.contentView = undefined`. That bypassed +page.js's
validContentTypes validation and let foreign `view` values through
(e.g. `view=list` preserved by buildCommunityPath when navigating from
/calendar after toggling grid/list). MainContentArea has no branch
for `list`, so the {#key} block rendered no view — blank content
area that the user perceived as an empty home feed. Now the derived
relies on the validated `data.contentView` only, defaulting to
`home` when undefined.
- ImageWithFallback: handleError() is wired to the <img> onerror DOM
handler, so it can fire after the component's $effect has been
destroyed (e.g. view switch while the image is still loading).
Reading the `robohashSrc` $derived at that point produced the
`[svelte] derived_inert` console warning. Since the value only
depends on the `src` prop — which remains accessible from the
closure — inline the template string and drop the $derived.
Adds console.debug instrumentation to confirm the feed renders for the user-reported reproduction path (/calendar → grid → list → click community) now that the routing root cause is fixed. - HomeView: logs emission index + length + dt-since-effect for both activitySub (CommunityActivityModel) and bookmarkSub (CommunitySocialBookmarkModel). Lets us see whether an empty first emit is followed by populated emits, or stays empty — useful if a network-failure-induced empty feed shows up separately from the already-fixed routing bug. - community-content-loader: counting subs for the `direct`, `reposts`, and `targetedPublications` timeline streams. Each logs its event count on complete so we can tell when the network layer returns nothing for a community. Safe to revert once we've confirmed no lingering empty-feed cases.
…p branch Renames CalendarFilterSidebar.svelte → CalendarFilterDrawer.svelte and rewrites it as a mobile-only overlay drawer. Removes the desktop collapsible sidebar branch entirely. Adds Authors section (FeaturedAuthors) as a fifth filter section. Props now received from parent instead of reading calendarFilters store internally. Updates CalendarView.svelte to point at the new file with a temporary stub for Task 9 to fully wire up.
…ne filters Wire CalendarFilterBar, CalendarFilterDrawer, and FeaturedAuthors into CalendarView, replacing the old flex-sidebar + rounded-card layout with a stacked container layout; add featuredAuthors default to config and Step 3 featured-author filtering to displayedEvents.
…thors rail Five Playwright tests covering the /calendar redesign: - Footer is visible at bottom (page no longer island-wrapped) - Page uses min-h-screen wrapper (old card flex wrapper gone) - Desktop inline filter bar renders its triggers - Mobile Filter button opens drawer; Escape closes - No critical JS errors on drawer flow - Featured Authors rail present when CALENDAR_FEATURED_AUTHORS is set (skips otherwise) Reset-link behavior is covered by component tests in CalendarFilterBar.test.js.
- TagSelector: free-text input + Add button (mirrors RelaySelector custom-relay UX) with normalization (#FooBar → foobar), duplicate detection, and inline error - SearchInput: refactor to a generic controlled input (value/onChange/onSubmit/ debounceMs) so it can be reused outside calendarFilters. Migrate CalendarFilterBar and CalendarFilterDrawer to wire the store themselves - PeopleFilter (new): SearchInput-driven name filter over the avatar grid + Enter-to-add by npub/hex, auto-opens details when a query is active - normalizePubkey: extract from ContactSearchInput into \$lib/helpers/pubkey.js (jsdom-safe, no app-settings transitive deps) - ActiveFilterChips, AdvancedFiltersDropdown: new components for the redesigned filter chrome; calendar-filters store gains onlyFollowsMode + effective-author helpers; calendar loaders gain date-range / search variants - i18n keys for tag selector and people filter (en/de) - Tests: pubkey (9), SearchInput (7), PeopleFilter (7), TagSelector (6), ActiveFilterChips, calendar-search, calendar-filters effective-authors and follow-lists, calendar-date-range-loader, calendar-relay-filter, plus E2E coverage for the redesigned chrome
Calendar UI redesign: - Inline filter bar + drawer chrome (replaces sidebar + card) - ActiveFilterChips, AdvancedFiltersDropdown, PeopleFilter components - Generic SearchInput (reusable beyond calendarFilters) - TagSelector custom-tag input - normalizePubkey helper extracted to \$lib/helpers - E2E coverage for the redesigned chrome
Skipped calendar-search.js — uses pool.request() directly for NIP-50 search.
Skipped amb-search.js (uses pool.request() directly for NIP-50 search).
Updated test mocks for base.js to expose createCachedTimelineLoader as a passthrough that calls the (mocked) createTimelineLoader, so existing assertions on createTimelineLoader call args still match.
Skipped profile.js, contact-list-loader.js, relay-list-loader.js, blossom-server-loader.js, badge-loaders.js — they take pool/eventStore as parameters instead of using the singleton, so the helper signature doesn't fit. They will keep using createTimelineLoader directly with the caller-supplied pool.
…svp loaders Skipped blossom-server-loader.js and badge-loaders.js — both take pool and eventStore as parameters instead of using the singleton. Updated vocab-loader test mock to expose createCachedTimelineLoader.
The NostrIDB constructor accesses indexedDB at instantiation time, which crashes the SvelteKit prerender step where indexedDB is undefined. Guard the singleton with `typeof indexedDB !== 'undefined'` and have the public API (cacheRequest/count/clear/warmIdentity, dbReady) early-return their graceful fallbacks when the singleton is unavailable. Aligns with the spec's "additive cache, never block the app" requirement.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a persistent event cache backed by IndexedDB (via
nostr-idb) so repeat visits feel snappy. Loaders read from cache first, network second; events flow back into the cache automatically.event-cache.svelte.jswrappingNostrIDBwith a kind allowlist (0, 3, 5, 10002, 10222, 30000, 30002, 30023, 30142, 31922, 31923, 31924)cacheRequestintoaddressLoader,eventLoader,unifiedLoadercreateCachedTimelineLoaderhelper used by calendar, educational, community, kanban, vocab, highlights, RSVP loaderswarmIdentityhydrates own kind-3 / 10002 / 30002 / 30000 events + followed users' profiles on boote2e/cache-warm-boot.test.js) confirms cards render after WS is blockedNostrIDBconstructed only whenindexedDBexistsSpec:
docs/superpowers/specs/2026-04-24-persistent-event-cache-design.mdTest plan
pnpm run test— 1950 pass (3 pre-existing failures in GlobalFAB/NoteCard, unrelated)pnpm run check— 0 errors, 2 warnings (both unrelated to this branch)pnpm run build— cleanpnpm run test:e2e cache-warm-boot— pass[event-cache]console warnings at any pointDeferred follow-ups
createTimelineLoaderdue to factory-pattern signature:relay-list-loader.js,contact-list-loader.js,userDeletionLoaderinbase.js. Writes still flow throughpersistEventsToCache, so functionality is intact — just missing read-side cache-hit benefit. Worth a follow-up PR.createDateRangeCalendarLoader/createPaginatedCalendarLoaderusetimedPooldirectly. Render fine via network; cache-hit benefit deferred.