Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.32] - 2026-07-24

### Changed
- Reused replacement-closure results while cloning repeated acyclic configuration aliases, reducing traversal work from quadratic to linear without changing merge identity or isolation semantics.

## [0.2.31] - 2026-07-23

### Fixed
Expand Down Expand Up @@ -263,7 +268,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Code quality tools including Ruff and mypy
- Documentation and browser extension setup guides

[Unreleased]: https://github.com/Enablement-Engineering/gobbler/compare/v0.2.31...HEAD
[Unreleased]: https://github.com/Enablement-Engineering/gobbler/compare/v0.2.32...HEAD
[0.2.32]: https://github.com/Enablement-Engineering/gobbler/compare/v0.2.31...v0.2.32
[0.2.31]: https://github.com/Enablement-Engineering/gobbler/compare/v0.2.30...v0.2.31
[0.2.30]: https://github.com/Enablement-Engineering/gobbler/compare/v0.2.29...v0.2.30
[0.2.29]: https://github.com/Enablement-Engineering/gobbler/compare/v0.2.28...v0.2.29
Expand Down
2 changes: 1 addition & 1 deletion browser-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ function connectWebSocket() {
// Send registration message
ws.send(JSON.stringify({
type: 'register',
extension_version: '0.2.31',
extension_version: '0.2.32',
}));

// Clear reconnect interval if it exists
Expand Down
2 changes: 1 addition & 1 deletion browser-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Gobbler",
"version": "0.2.31",
"version": "0.2.32",
"description": "Extract, navigate, and automate tabs in Gobbler's managed browser group",
"permissions": [
"activeTab",
Expand Down
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ icon: material/history

All notable changes to this project are tracked in the root [CHANGELOG.md](https://github.com/Enablement-Engineering/gobbler/blob/main/CHANGELOG.md).

## [0.2.32] - 2026-07-24

### Changed

- Reused replacement-closure results while cloning repeated acyclic configuration aliases, reducing traversal work from quadratic to linear without changing merge identity or isolation semantics.

## [0.2.31] - 2026-07-23

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "gobbler"
version = "0.2.31"
version = "0.2.32"
description = "Universal content conversion fabric - convert YouTube, web, documents, audio/video to markdown via CLI and skills"
authors = [
{name = "Dylan Isaac", email = "dylan@enablement.engineering"}
Expand Down
2 changes: 1 addition & 1 deletion src/gobbler_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
$ gobbler relay start
"""

__version__ = "0.2.31"
__version__ = "0.2.32"

__all__ = ["__version__"]
2 changes: 1 addition & 1 deletion src/gobbler_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
Converters live in this package and are used by the CLI.
"""

__version__ = "0.2.31"
__version__ = "0.2.32"

# Convenience imports for common use cases
from gobbler_core.providers.youtube import (
Expand Down
21 changes: 18 additions & 3 deletions src/gobbler_core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,14 @@ def _clone_projected_override(
active: dict[int, dict[str, Any]],
shared: dict[int, Any],
projection: dict[int, Any],
replacement_cache: dict[int, set[int]],
) -> Any:
"""Clone a value, isolating only paths that reference an active merge projection."""
marked = _ConfigLoader._replacement_closure(value, active)
value_id = id(value)
marked = replacement_cache.get(value_id)
if marked is None:
marked = _ConfigLoader._replacement_closure(value, active)
replacement_cache[value_id] = marked
if id(value) not in marked:
return Config._clone_override_value(value, active, shared)

Expand Down Expand Up @@ -689,11 +694,19 @@ def _deep_merge(
int,
dict[str, Any],
dict[int, Any],
dict[int, set[int]],
tuple[int, int] | None,
]
] = [(iter(override.items()), override_id, result, {}, None)]
] = [(iter(override.items()), override_id, result, {}, {}, None)]
while stack:
iterator, current_override_id, target, projection, completed_key = stack[-1]
(
iterator,
current_override_id,
target,
projection,
replacement_cache,
completed_key,
) = stack[-1]
try:
key, value = next(iterator)
except StopIteration:
Expand Down Expand Up @@ -727,6 +740,7 @@ def _deep_merge(
value_id,
child,
{},
{},
pair if cacheable else None,
)
)
Expand All @@ -737,6 +751,7 @@ def _deep_merge(
active,
cloned,
projection,
replacement_cache,
)
return result

Expand Down
2 changes: 1 addition & 1 deletion src/gobbler_queue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from .models import Job, JobStatus, JobSummary, JobType
from .worker import Worker

__version__ = "0.2.31"
__version__ = "0.2.32"

__all__ = [
"Database",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_browser_extension_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_background_command_contract_matches_relay_client_helpers() -> None:
assert "command_id: command_id" in background
assert "result: result" in background
assert "type: 'register'" in background
assert "extension_version: '0.2.31'" in background
assert "extension_version: '0.2.32'" in background
assert "type: 'ping'" in background
assert "message.type === 'pong'" in background

Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,55 @@ def test_cycle_detection_memoizes_matching_cycle_members(self) -> None:
assert Config._has_container_cycle(nodes[0], cache)
assert all(cache[id(node)] for node in nodes)

def test_deep_merge_clones_shared_acyclic_payload_once(self) -> None:
"""Test repeated aliases do not retraverse an already cloned payload."""

class CountingList(list[object]):
yielded = 0

def __iter__(self):
for item in super().__iter__():
type(self).yielded += 1
yield item

alias_count = 80
payload = CountingList([[] for _index in range(alias_count)])

merged = Config._deep_merge(
{},
{str(index): payload for index in range(alias_count)},
)

assert all(merged[str(index)] is merged["0"] for index in range(alias_count))
assert merged["0"] is not payload
assert CountingList.yielded <= alias_count * 5

def test_deep_merge_reuses_alias_scan_after_sibling_mapping_merges(self) -> None:
"""Test sibling merge frames do not evict an outer frame's alias scan."""

class CountingList(list[object]):
yielded = 0

def __iter__(self):
for item in super().__iter__():
type(self).yielded += 1
yield item

alias_count = 80
payload = CountingList([[] for _index in range(alias_count)])
base: dict[str, object] = {}
override: dict[str, object] = {}
for index in range(alias_count):
override[f"alias_{index}"] = payload
base[f"merge_{index}"] = {"base": index}
override[f"merge_{index}"] = {"override": index}

merged = Config._deep_merge(base, override)

assert all(merged[f"alias_{index}"] is merged["alias_0"] for index in range(alias_count))
assert merged["alias_0"] is not payload
assert CountingList.yielded <= alias_count * 5

def test_deep_merge_clones_shared_empty_mutable_overrides(self) -> None:
"""Test empty mutable override containers are cloned once without mutating input."""
shared_list: list[object] = []
Expand Down
Loading