Skip to content

perf: merchant view#790

Merged
escapedcat merged 15 commits intomainfrom
perf/merchant-view
Mar 5, 2026
Merged

perf: merchant view#790
escapedcat merged 15 commits intomainfrom
perf/merchant-view

Conversation

@escapedcat
Copy link
Contributor

@escapedcat escapedcat commented Mar 4, 2026

Summary by CodeRabbit

  • New Features

    • Utilities to build OSM-style tags and derive contact, payment, and boosted info from place data.
    • Cache-aware place update that syncs local cache and in-memory store.
    • Client-side caching to speed verified-date computations.
  • Bug Fixes

    • Deleted places are now removed reliably from local cache.
  • Refactor

    • Merchant page switched to reactive bindings and updated cache-sync flow to use the new cache-aware update.
    • Map API types and signatures refined; in-module caching added for verified-date lookups.

…PI call

- Added new function that updates localforage cache with provided Place data
- Eliminates redundant API call when we already have server-fetched data
- Type-safe implementation with proper error handling
…data copying

- Use updatePlaceInCache with server data instead of redundant API call
- Replace initializeData data copying with $: reactive declarations
- Simplifies code and leverages Svelte's reactivity properly
- Removes unnecessary local variable duplications
@netlify
Copy link

netlify bot commented Mar 4, 2026

Deploy Preview for btcmap ready!

Name Link
🔨 Latest commit cae6915
🔍 Latest deploy log https://app.netlify.com/projects/btcmap/deploys/69a99ef1753b9f0008a3bd36
😎 Deploy Preview https://deploy-preview-790--btcmap.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 65 (🔴 down 30 from production)
Accessibility: 97 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 99 (no change from production)
PWA: 90 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Optimize merchant page with reactive declarations and cache updates

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add updatePlaceInCache function to update cache with server data without API call
• Replace manual data copying with Svelte reactive declarations in merchant page
• Eliminate redundant API call by using pre-fetched server data directly
• Simplify component logic and leverage Svelte's reactivity system
Diagram
flowchart LR
  A["Server Data"] -->|"updatePlaceInCache"| B["LocalForage Cache"]
  A -->|"Reactive Declarations"| C["Component Variables"]
  D["initializeData"] -->|"Simplified"| C
  E["updateSinglePlace"] -->|"Replaced with"| F["updatePlaceInCache"]
Loading

Grey Divider

File Changes

1. src/routes/merchant/[id]/+page.svelte ✨ Enhancement +25/-48

Convert to reactive declarations and optimize cache updates

• Replaced updateSinglePlace import with updatePlaceInCache
• Converted manual variable assignments in initializeData to Svelte reactive declarations using
 $: syntax
• Removed redundant local variable declarations for merchant data fields
• Changed cache update call to use pre-fetched data.placeData instead of making API call

src/routes/merchant/[id]/+page.svelte


2. src/lib/sync/places.ts ✨ Enhancement +43/-0

Add updatePlaceInCache function for direct cache updates

• Added new updatePlaceInCache function that updates localforage cache with provided Place data
• Handles both updating existing places and adding new ones to cache
• Supports deletion by filtering out places with deleted_at flag
• Includes proper error handling and store synchronization via yieldToMain

src/lib/sync/places.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 4, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Undeclared reactive variables 📘 Rule violation ✓ Correctness
Description
Reactive assignments set variables like icon, hours, and merchantEvents without any
let/const declarations, which will cause Svelte/TypeScript validation errors. This can break
yarn run check and prevent the page from compiling.
Code

src/routes/merchant/[id]/+page.svelte[R121-141]

+$: icon = data.icon;
+$: address = data.address;
+$: description = data.description;
+
+$: hours = data.hours;
+$: payment = data.payment;
+$: phone = data.phone;
+$: website = data.website;
+$: email = data.email;
+$: twitter = data.twitter;
+$: instagram = data.instagram;
+$: facebook = data.facebook;
+
+$: thirdParty = data.thirdParty;
+$: paymentMethod = data.paymentMethod;
+
+$: lat = data.lat;
+$: long = data.lon;

-let hours: string | undefined;
-let payment: PayMerchant;
+$: filteredCommunities = data.areas;
+$: merchantEvents = data.activity;
Evidence
PR Compliance ID 2 requires yarn run check to pass. The PR introduces $: assignments to
identifiers (e.g., icon, hours, merchantEvents) that are not declared anywhere in the
component, which is a compile/type-check error in Svelte/TypeScript.

AGENTS.md
src/routes/merchant/[id]/+page.svelte[120-143]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`$:` reactive assignments in `+page.svelte` assign to variables (e.g., `icon`, `hours`, `merchantEvents`) that are not declared with `let`/`const`, which will fail Svelte/TypeScript checking.

## Issue Context
The previous `let` declarations for these fields were removed in this PR, but the template and logic still rely on these variables. Svelte requires variables to be declared in the `<script>` scope before use.

## Fix Focus Areas
- src/routes/merchant/[id]/+page.svelte[121-141]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Racy places_v4 writes 🐞 Bug ⛯ Reliability
Description
updatePlaceInCache() introduces an additional writer to localforage key places_v4 and places
store without coordination with elementsSync() / updateSinglePlace(). Because elementsSync()
schedules localforage.setItem(...).then(...) without awaiting it and clears syncInProgress in
finally, writes can interleave and last-writer-wins can clobber fresher data in both persistent
cache and in-memory store.
Code

src/lib/sync/places.ts[R452-486]

+export const updatePlaceInCache = async (
+	place: Place,
+): Promise<Place | null> => {
+	try {
+		const cachedPlaces = await localforage.getItem<Place[]>("places_v4");
+
+		if (!cachedPlaces) {
+			console.warn("No cached places found, cannot update place");
+			return null;
+		}
+
+		if (place.deleted_at) {
+			const updatedPlaces = cachedPlaces.filter((p) => p.id !== place.id);
+			if (updatedPlaces.length !== cachedPlaces.length) {
+				await localforage.setItem("places_v4", updatedPlaces);
+				await yieldToMain();
+				places.set(updatedPlaces);
+				console.info(`Removed deleted place ${place.id} from cache`);
+			}
+			return null;
+		}
+
+		const placeIndex = cachedPlaces.findIndex((p) => p.id === place.id);
+
+		let updatedPlaces: Place[];
+		if (placeIndex !== -1) {
+			updatedPlaces = [...cachedPlaces];
+			updatedPlaces[placeIndex] = place;
+		} else {
+			updatedPlaces = [...cachedPlaces, place];
+		}
+
+		await localforage.setItem("places_v4", updatedPlaces);
+		await yieldToMain();
+		places.set(updatedPlaces);
Evidence
updatePlaceInCache() reads-modifies-writes the entire cached places array and then updates the
global places store. elementsSync() also writes the full placesData to the same places_v4
key using a non-awaited promise chain and updates the same store later. Since elementsSync() uses
a syncInProgress flag but clears it in finally regardless of whether its
setItem(...).then(...) callbacks have finished, other cache writers (including the newly added
updatePlaceInCache() call on merchant page mount) can run concurrently during the window where the
sync is still persisting/updating, creating a real lost-update risk.

src/lib/sync/places.ts[452-488]
src/lib/sync/places.ts[305-338]
src/lib/sync/places.ts[88-96]
src/lib/sync/places.ts[383-385]
src/routes/merchant/[id]/+page.svelte[236-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`places_v4` (localforage) and the `places` store have multiple writers (`elementsSync`, `updateSinglePlace`, `updatePlaceInCache`) that can run concurrently. Because `elementsSync` schedules its `localforage.setItem(...).then(...)` without awaiting completion and clears `syncInProgress` in `finally`, other writers can interleave and overwrite newer data.

## Issue Context
- `updatePlaceInCache()` is newly added and is called on merchant page mount.
- `elementsSync()` writes the same key and store using a non-awaited promise chain.

## Fix Focus Areas
- src/lib/sync/places.ts[88-96]
- src/lib/sync/places.ts[305-338]
- src/lib/sync/places.ts[383-385]
- src/lib/sync/places.ts[388-446]
- src/lib/sync/places.ts[452-487]
- src/routes/merchant/[id]/+page.svelte[236-252]

## Suggested approach
1. Add a module-level async mutex/queue (e.g., a simple promise chain) for all operations that mutate `places_v4` and call `places.set(...)`.
2. Refactor `elementsSync()` to `await` the persistence + store update work (avoid fire-and-forget `.then(...)`) so `syncInProgress` reflects the true in-flight duration.
3. Wrap `updateSinglePlace()` and `updatePlaceInCache()` in the same mutex so their read-modify-write cycles are serialized with sync writes.
4. Optionally: if `elementsSync()` is in progress, either wait for it or re-read `places_v4` after it completes before applying the single-place update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@escapedcat escapedcat changed the title Perf/merchant view perf: merchant view Mar 4, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an exported cache-update function for places, refactors the merchant page to use reactive bindings and dynamically call the new cache updater, introduces place→OSM tag transformation utilities, and adds an in-module verified-date cache in the map setup.

Changes

Cohort / File(s) Summary
Cache update
src/lib/sync/places.ts
New exported updatePlaceInCache(place: Place) — reads/writes places_v4 in localforage, handles deletions, updates/appends entries, yields to main thread, and updates the places store.
Merchant page (client)
src/routes/merchant/[id]/+page.svelte
Switched many fields to reactive $: bindings; replaced direct updateSinglePlace import with a dynamic import of updatePlaceInCache and call on mount.
Merchant page (server)
src/routes/merchant/[id]/+page.server.ts
Replaced manual extraction of payment/contact/boost/osm tags with helpers from src/lib/transforms/place; added osmTags and derived fields to returned data; enabled include_deleted=true on API request.
Transforms
src/lib/transforms/place.ts
New module exporting ContactFields, getContactFields, mapPayment, getBoosted, getPaymentMethod, and buildOsmTags to produce OSM-like tags and payment/contact mappings from a Place.
Map helper
src/lib/map/setup.ts
Renamed Leaflet Map type alias to LeafletMap in signatures and added verifiedCache with MRU eviction plus explicit verifiedArr(place): string[] caching.
Types/imports
src/routes/merchant/[id]/+page.svelte, src/routes/merchant/[id]/+page.server.ts
Adjusted type import paths ($lib/types) and updated imports to use new transform helpers and updated sync API usage.

Sequence Diagram

sequenceDiagram
    participant Merchant as Merchant Page
    participant Cache as LocalForage (places_v4)
    participant Main as Main Thread
    participant Store as Places Store

    Merchant->>Cache: read `places_v4`
    Cache-->>Merchant: return array

    alt place.deleted_at set
        Merchant->>Merchant: remove place from array
    else place exists
        Merchant->>Merchant: update existing place entry
    else new place
        Merchant->>Merchant: append place to array
    end

    Merchant->>Cache: write updated `places_v4`
    Cache-->>Merchant: confirm write

    Merchant->>Main: yieldToMain()
    Main-->>Merchant: resume

    Merchant->>Store: places.set(updated array)
    Store-->>Merchant: store updated

    Merchant-->>Merchant: return updated Place | null
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Suggested reviewers

  • bubelov
  • dadofsambonzuki

Poem

🐇 I hopped through cache and tag-filled lands,
I nudged a place with gentle hands,
I stitched the tags and cached the dates,
The merchant wakes — no stale updates,
Hop, sync, and bloom — hooray for fast relands!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided; the author did not fill in any sections of the required template. Add a description explaining the changes: the new updatePlaceInCache function, switch to reactive bindings, import of new transform utilities, and the include_deleted=true fix for deleted places.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf: merchant view' is partially related to the changeset, referring to performance improvements, but it lacks specificity about the actual changes (cache optimization, reactive bindings, new utility functions).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/merchant-view

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/sync/places.ts (1)

452-493: Reduce duplicated cache-mutation logic between single-place update paths.

updatePlaceInCache now duplicates most of updateSinglePlace’s delete/upsert/persist/store-update flow. This increases drift risk when one path changes and the other doesn’t.

♻️ Suggested refactor (shared helper)
+const applyPlaceToCache = async (incomingPlace: Place): Promise<Place | null> => {
+	const cachedPlaces = await localforage.getItem<Place[]>("places_v4");
+	if (!cachedPlaces) return null;
+
+	if (incomingPlace.deleted_at) {
+		const updatedPlaces = cachedPlaces.filter((p) => p.id !== incomingPlace.id);
+		if (updatedPlaces.length !== cachedPlaces.length) {
+			await localforage.setItem("places_v4", updatedPlaces);
+			await yieldToMain();
+			places.set(updatedPlaces);
+		}
+		return null;
+	}
+
+	const placeIndex = cachedPlaces.findIndex((p) => p.id === incomingPlace.id);
+	const updatedPlaces =
+		placeIndex !== -1
+			? [...cachedPlaces.slice(0, placeIndex), incomingPlace, ...cachedPlaces.slice(placeIndex + 1)]
+			: [...cachedPlaces, incomingPlace];
+
+	await localforage.setItem("places_v4", updatedPlaces);
+	await yieldToMain();
+	places.set(updatedPlaces);
+	return incomingPlace;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/sync/places.ts` around lines 452 - 493, updatePlaceInCache duplicates
the cache mutation flow found in updateSinglePlace (delete vs upsert, persist to
localforage, await yieldToMain, and places.set), so extract that shared behavior
into a single helper (e.g., upsertOrRemovePlaceInCache or syncPlaceToCache) that
accepts the Place and cachedPlaces array and performs: 1) remove when
place.deleted_at is set (filter, persist, yieldToMain, places.set) 2) upsert
otherwise (replace or append, persist, yieldToMain, places.set) and returns the
resulting Place or null; then replace the duplicated blocks in
updatePlaceInCache and updateSinglePlace to call this helper and keep their
try/catch flow intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/merchant/`[id]/+page.svelte:
- Line 143: The current const assignment "const name = data.name" will not
update when the page's "data" object changes; change it to a reactive
declaration so the variable tracks updates (e.g., replace the const with a
reactive binding such as using the Svelte reactive label to set "name" from
"data.name"); update the declaration that sets "name" to mirror the pattern used
for other fields in this component so headings/meta text and i18n values stay in
sync with "data".

---

Nitpick comments:
In `@src/lib/sync/places.ts`:
- Around line 452-493: updatePlaceInCache duplicates the cache mutation flow
found in updateSinglePlace (delete vs upsert, persist to localforage, await
yieldToMain, and places.set), so extract that shared behavior into a single
helper (e.g., upsertOrRemovePlaceInCache or syncPlaceToCache) that accepts the
Place and cachedPlaces array and performs: 1) remove when place.deleted_at is
set (filter, persist, yieldToMain, places.set) 2) upsert otherwise (replace or
append, persist, yieldToMain, places.set) and returns the resulting Place or
null; then replace the duplicated blocks in updatePlaceInCache and
updateSinglePlace to call this helper and keep their try/catch flow intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f58a8321-f5c4-4107-8c47-47c1e20f86cc

📥 Commits

Reviewing files that changed from the base of the PR and between 41588a3 and f4b9425.

📒 Files selected for processing (2)
  • src/lib/sync/places.ts
  • src/routes/merchant/[id]/+page.svelte

- Create /transforms/place.ts with reusable functions:
  - mapPayment(): Maps payment fields to PayMerchant type
  - getBoosted(): Calculates boosted status
  - getContactFields(): Gets contact with fallback to osm:contact fields
  - getPaymentMethod(): Concatenates payment method strings
  - buildOsmTags(): Builds OSM tags object for tag modal
- Update merchant detail page server to use new functions
- Reduces code duplication and improves maintainability
- Add verifiedCache WeakMap to memoize verifiedArr results
- WeakMap allows garbage collection when Place objects are no longer referenced
- Avoids re-computing verified dates on every render in components
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/lib/transforms/place.ts (1)

3-10: Use type instead of interface for ContactFields.

Per coding guidelines, prefer type over interface for new type definitions in TypeScript.

Suggested fix
-export interface ContactFields {
-	phone?: string;
-	website?: string;
-	email?: string;
-	twitter?: string;
-	instagram?: string;
-	facebook?: string;
-}
+export type ContactFields = {
+	phone?: string;
+	website?: string;
+	email?: string;
+	twitter?: string;
+	instagram?: string;
+	facebook?: string;
+};

As per coding guidelines: "Prefer type over interface for new type definitions in TypeScript; use interface only when needed (declaration merging or class implementation)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/transforms/place.ts` around lines 3 - 10, Replace the exported
interface ContactFields with an exported type alias that preserves the same
optional string properties (phone, website, email, twitter, instagram,
facebook); update any references to ContactFields to continue using the same
name (no API change) and ensure the exported declaration reads "export type
ContactFields = { ... }" with the same property signatures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/transforms/place.ts`:
- Around line 3-10: Replace the exported interface ContactFields with an
exported type alias that preserves the same optional string properties (phone,
website, email, twitter, instagram, facebook); update any references to
ContactFields to continue using the same name (no API change) and ensure the
exported declaration reads "export type ContactFields = { ... }" with the same
property signatures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 98d9378c-4642-4318-ad12-7f11b917c895

📥 Commits

Reviewing files that changed from the base of the PR and between f4b9425 and c4c1dc5.

📒 Files selected for processing (3)
  • src/lib/map/setup.ts
  • src/lib/transforms/place.ts
  • src/routes/merchant/[id]/+page.server.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/map/setup.ts (1)

589-620: ⚠️ Potential issue | 🟠 Major

Cache key is too coarse; can serve stale verification dates and leak memory over time.

Line 594 returns cached data by place.id only. If fresh API data for the same place has updated verification fields, this still returns old values. Also, this module-scope cache has no eviction bound.

Proposed fix (cache by content signature, not only id)
-// Cache verification arrays per place id; entries are overwritten whenever fresh Place data arrives via stores
-const verifiedCache = new Map<number, string[]>();
+type VerifiedCacheEntry = { signature: string; values: string[] };
+const verifiedCache = new Map<number, VerifiedCacheEntry>();
+
+const verifiedSignature = (place: Place): string =>
+	[
+		place["osm:survey:date"] ?? "",
+		place["osm:check_date"] ?? "",
+		place["osm:check_date:currency:XBT"] ?? "",
+	].join("|");

 export const verifiedArr = (place: Place): string[] => {
 	const cacheKey = place.id;
-	if (verifiedCache.has(cacheKey)) {
-		return verifiedCache.get(cacheKey)!;
-	}
+	const signature = verifiedSignature(place);
+	const cached = verifiedCache.get(cacheKey);
+	if (cached && cached.signature === signature) return cached.values;

 	const verified: string[] = [];
@@
-	verifiedCache.set(cacheKey, verified);
+	verifiedCache.set(cacheKey, { signature, values: verified });

 	return verified;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/map/setup.ts` around lines 589 - 620, The current verifiedCache
(module-scope Map) keyed only by place.id causes stale results and unbounded
memory growth; update verifiedArr to compute a content signature from the
relevant verification fields (e.g., concatenate or hash
place["osm:survey:date"], place["osm:check_date"],
place["osm:check_date:currency:XBT"] and/or a place.lastUpdated field) and use a
composite cache key like `${place.id}:${signature}` when reading/writing
verifiedCache, so new API data with changed verification fields produces a
different cache entry; also add a simple eviction bound on verifiedCache (e.g.,
cap size and delete oldest entries or implement an LRU policy) to prevent memory
leaks and ensure stale entries are removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/lib/map/setup.ts`:
- Around line 589-620: The current verifiedCache (module-scope Map) keyed only
by place.id causes stale results and unbounded memory growth; update verifiedArr
to compute a content signature from the relevant verification fields (e.g.,
concatenate or hash place["osm:survey:date"], place["osm:check_date"],
place["osm:check_date:currency:XBT"] and/or a place.lastUpdated field) and use a
composite cache key like `${place.id}:${signature}` when reading/writing
verifiedCache, so new API data with changed verification fields produces a
different cache entry; also add a simple eviction bound on verifiedCache (e.g.,
cap size and delete oldest entries or implement an LRU policy) to prevent memory
leaks and ensure stale entries are removed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3514b136-671f-4a2c-892c-13f9bb29da1e

📥 Commits

Reviewing files that changed from the base of the PR and between c4c1dc5 and a6671f3.

📒 Files selected for processing (2)
  • src/lib/map/setup.ts
  • src/lib/transforms/place.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/transforms/place.ts

@bubelov
Copy link
Collaborator

bubelov commented Mar 5, 2026

I opened a few merchants from the map view and the performance was clearly better than before, near instant in many cases, but when I tried jumping to a few random places directly by typing their ids into an address bar, most of them failed to load.

This link works:

https://btcmap.org/merchant/7361

While the same preview link doesn't:

https://deploy-preview-790--btcmap.netlify.app/merchant/7361

Not sure if related to this RR or Netlify preview env quirks

Screenshot From 2026-03-05 11-12-19

- Add LRU cache size management (MAX_CACHE_SIZE=100) to verifiedArr to prevent unbounded memory growth
- Add SSR browser guard to updatePlaceInCache for runtime safety
- Convert updatePlaceInCache to dynamic import in onMount for explicit client-only loading
- Fix mapPayment return type to PayMerchant | undefined
- Remove unused updatePlaceInCache import

🤖 Generated with [opencode](https://opencode.ai)
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR focuses on improving the merchant detail view’s performance and data handling by moving repeated place-field derivations into shared utilities, introducing a cache-aware place update helper, and adjusting merchant page data bindings.

Changes:

  • Refactors merchant server-load processing to use new $lib/transforms/place helpers (OSM tags, contact fields, payment mapping, boosted state).
  • Adds updatePlaceInCache(place) to update LocalForage + in-memory places store using already-fetched Place data.
  • Adds per-place caching for verifiedArr computations and updates Leaflet Map type imports to avoid name conflicts.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/routes/merchant/[id]/+page.svelte Switches merchant page to reactive bindings and uses the new cache update helper on mount.
src/routes/merchant/[id]/+page.server.ts Refactors place-derived fields (payment/contact/osmTags/boosted) into shared transform utilities.
src/lib/transforms/place.ts New utility module for deriving contact/payment/boosted state and building OSM-style tags from Place.
src/lib/sync/places.ts Adds updatePlaceInCache for updating LocalForage and the places store using an existing Place object.
src/lib/map/setup.ts Renames Leaflet Map import to LeafletMap and introduces caching for verifiedArr.

…Tags

Using the existing OSMTags type directly removes the explicit-any usage
and ensures the intermediate accumulator is consistent with the return type.

🤖 Generated with [opencode](https://opencode.ai)
Reactive $: assignments require prior let declarations in Svelte/TypeScript.
Restores explicit types for all fields derived from server data and brings
back safe [] defaults for merchantEvents and filteredCommunities so
downstream slice/iteration cannot throw on first render.

Also converts const name to a reactive declaration so it tracks data updates.

🤖 Generated with [opencode](https://opencode.ai)
… entries

A Place object with the same id but newer verification dates would
previously return the first-computed array forever. Keying on
id + updated_at means any update to the Place naturally produces a
cache miss and a fresh computation. Falls back to id-only when
updated_at is absent.

🤖 Generated with [opencode](https://opencode.ai)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/transforms/place.ts`:
- Around line 23-48: mapPayment and getPaymentMethod only read osm:payment:*
keys and miss canonical payment:* fields; update both to check canonical keys
first and fall back to osm:payment:* equivalents (e.g., check
place["payment:uri"] then place["osm:payment:uri"], same for payment:pouch and
payment:coinos) so payment links/icons aren’t lost, and extend getPaymentMethod
to include "payment:onchain", "payment:lightning", and
"payment:lightning_contactless" (checking canonical then osm-prefixed keys) when
resolving the method.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: babdd0da-49ed-40d1-b3de-6bd8b8e25c5b

📥 Commits

Reviewing files that changed from the base of the PR and between a6671f3 and 91a6f20.

📒 Files selected for processing (4)
  • src/lib/map/setup.ts
  • src/lib/sync/places.ts
  • src/lib/transforms/place.ts
  • src/routes/merchant/[id]/+page.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/sync/places.ts

Deleted places return only {id} without include_deleted=true, causing
a 500 when the server load tries to access lat, lon, icon and other
fields on the minimal response object.

🤖 Generated with [opencode](https://opencode.ai)
DOMPurify requires a DOM environment and crashes during server-side
rendering. Skip sanitization on the server — the input is OSM
opening_hours data, not arbitrary user HTML, so this is safe.

Fixes 500 error when viewing deleted merchants (and any merchant with
opening_hours during SSR).

🤖 Generated with [opencode](https://opencode.ai)
Verifies that deleted merchants return 200 (not 500), display the
removal notice banner, show the merchant name with (Deleted) suffix,
and still render the map container.

🤖 Generated with [opencode](https://opencode.ai)
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

- formatOpeningHours now uses simple escapeHtml instead of DOMPurify,
  which is SSR-safe and prevents XSS vulnerabilities
- escapeHtml function refactored to use simple string replace instead
  of document.createElement (which breaks on SSR)
- Removed unused DOMPurify import

This fixes the XSS vulnerability where malicious opening_hours data
containing </span><script>... could be executed during SSR.

🤖 Generated with [opencode](https://opencode.ai)
bubelov added a commit that referenced this pull request Mar 5, 2026
Removed 8 fields from Place type:
- line: unused OSM field
- payment:uri, payment:pouch, payment:coinos: derived fields (created in buildOsmTags)
- payment:lightning, payment:onchain, payment:lightning_contactless: derived fields
- osm:payment:lightning:requires_companion_app: never requested or used

These fields were either never populated by the API or were derived
fields that shouldn't have been in the Place type.

🤖 Generated with [opencode](https://opencode.ai)
The e2e tests depend on merchant ID 7361 remaining in the API with
deleted_at set. If this merchant gets purged in the future, the tests
will need a different deleted merchant ID.

🤖 Generated with [opencode](https://opencode.ai)
@escapedcat escapedcat merged commit 949e73a into main Mar 5, 2026
12 checks passed
@escapedcat escapedcat deleted the perf/merchant-view branch March 5, 2026 15:23
escapedcat pushed a commit that referenced this pull request Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants