Skip to main content

Bookmark Management

The BookmarkService in etchblok-test-api acts as the central orchestrator for all bookmark, tag, and collection operations. It provides a high-level facade that coordinates data persistence via the BookmarkRepository, full-text search through the SearchIndex, and performance optimization using an LRUCache.

Centralized Bookmark Operations

When you interact with bookmarks in etchblok-test-api, the BookmarkService ensures that business rules—such as URL validation and cache consistency—are applied uniformly. For example, in app/routes/bookmarks.py, the route handlers delegate directly to the service rather than manipulating models or the database themselves.

Creating and Updating Bookmarks

To create a bookmark, you pass a dictionary of data to create_bookmark. The service validates the input before persisting it.

# Example usage in app/routes/bookmarks.py
@bookmarks_bp.route("/", methods=["POST"])
def create_bookmark():
data = request.get_json(force=True)
bookmark, error = _service.create_bookmark(data)
if error:
return jsonify({"error": error}), 400
return jsonify(bookmark.to_dict()), 201

Internally, create_bookmark performs several steps:

  1. Validation: It calls _validate_url and _validate_title to ensure the data is well-formed.
  2. Persistence: It saves the Bookmark object via self._repo.save_bookmark(bookmark).
  3. Indexing: It immediately updates the search index using self._search.index_bookmark(bookmark).
  4. Cache Invalidation: It clears any existing cache entry for that ID via self._cache.invalidate(bookmark.id).

Updates follow a similar pattern in update_bookmark, where the service performs partial updates, re-validates changed fields, and refreshes both the search index and the cache.

Lifecycle Management: Soft-Deletes and Archiving

etchblok-test-api distinguishes between "trashing" a bookmark and "archiving" it.

  • Soft-Delete: The delete_bookmark method does not remove the record from the database. Instead, it calls bookmark.trash(), which marks it as trashed.
  • Archiving: The archive_bookmark method moves a bookmark to an archived state.
  • Restoration: The restore_bookmark method can bring a bookmark back to active status from either the trash or the archive.

In all these cases, the service ensures the LRUCache is invalidated so that subsequent fetches reflect the new status.

Tag Management and Cascading Updates

The BookmarkService handles the complex relationship between tags and bookmarks. While tags are independent entities, deleting one has side effects on the bookmarks that use it.

When you call delete_tag(tag_id), the service performs a cascading cleanup:

def delete_tag(self, tag_id: str) -> bool:
tag = self._repo.get_tag(tag_id)
if not tag:
return False
# Remove the tag reference from every bookmark that uses it
for bookmark in self._repo.get_bookmarks_with_tag(tag_id):
bookmark.remove_tag(tag_id)
self._repo.save_bookmark(bookmark)
self._cache.invalidate(bookmark.id)
# Finally, delete the tag itself
self._repo.delete_tag(tag_id)
return True

This logic ensures that no bookmark is left with a "dangling" reference to a non-existent tag.

Organizing with Collections

Collections allow you to group bookmarks. The BookmarkService provides methods to manage these groups and their memberships.

  • create_collection(data): Validates that a name is provided before saving.
  • add_to_collection(collection_id, bookmark_id): Retrieves the collection and uses its internal add_bookmark logic before persisting the change.
  • remove_from_collection(collection_id, bookmark_id): Similarly manages the removal of a bookmark from a collection.

To maintain responsiveness, BookmarkService integrates caching and specialized search indexing.

LRU Caching

The service maintains an internal LRUCache with a default max_size of 256. When you call get_bookmark(bookmark_id), the service checks the cache first:

def get_bookmark(self, bookmark_id: str) -> Optional[Bookmark]:
cached = self._cache.get(bookmark_id)
if cached is not None:
return cached
bookmark = self._repo.get_bookmark(bookmark_id)
if bookmark:
self._cache.put(bookmark.id, bookmark)
return bookmark

This reduces database load for frequently accessed bookmarks. The service manually invalidates cache entries during any write operation (update, delete, archive, etc.) to prevent stale data.

The search(query, limit) method delegates to the SearchIndex. By default, search results are limited to 20 items. The search index is updated in real-time whenever a bookmark is created or updated through the service.

Internal Architecture

Singleton Pattern

BookmarkService is implemented as a singleton using the __new__ method. This ensures that the cache and search index are shared across all parts of the etchblok-test-api application, such as different Flask blueprints.

_instance: Optional["BookmarkService"] = None

def __new__(cls) -> "BookmarkService":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._init_services()
return cls._instance

Service Initialization

The _init_services method bootstraps the internal components. It instantiates the BookmarkRepository, the LRUCache, and the SearchIndex. For testing purposes, the _reset() method can be used to re-initialize these components and clear the state.