graphrefly 0.6.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {graphrefly-0.6.0 → graphrefly-0.8.0}/CHANGELOG.md +19 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/PKG-INFO +1 -1
- {graphrefly-0.6.0 → graphrefly-0.8.0}/docs/optimizations.md +33 -18
- {graphrefly-0.6.0 → graphrefly-0.8.0}/docs/roadmap.md +6 -6
- {graphrefly-0.6.0 → graphrefly-0.8.0}/pyproject.toml +2 -1
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/__init__.py +2 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/compat/trio_runner.py +1 -1
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/__init__.py +4 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/dynamic_node.py +33 -12
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/node.py +57 -16
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/protocol.py +3 -3
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/sugar.py +4 -1
- graphrefly-0.8.0/src/graphrefly/core/timer.py +43 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/adapters.py +116 -69
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/cascading_cache.py +17 -10
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/resilience.py +28 -49
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/graph/graph.py +38 -10
- graphrefly-0.8.0/src/graphrefly/integrations/django.py +584 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/integrations/fastapi.py +45 -17
- graphrefly-0.8.0/src/graphrefly/patterns/__init__.py +13 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/ai.py +2 -2
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/cqrs.py +29 -26
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +1 -1
- graphrefly-0.8.0/src/graphrefly/patterns/reduction.py +605 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_core.py +4 -1
- graphrefly-0.8.0/tests/test_django.py +436 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_resilience.py +12 -2
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_sources.py +10 -6
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_fastapi.py +5 -3
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_graph.py +6 -6
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_guard.py +4 -1
- graphrefly-0.8.0/tests/test_patterns_reduction.py +419 -0
- graphrefly-0.8.0/tests/test_reduction.py +421 -0
- graphrefly-0.6.0/src/graphrefly/patterns/__init__.py +0 -5
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.claude/skills/dev-dispatch/SKILL.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.claude/skills/parity/SKILL.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.claude/skills/qa/SKILL.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.gemini/skills/dev-dispatch/SKILL.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.gemini/skills/parity/SKILL.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.github/workflows/pages.yml +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.github/workflows/release.yml +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.gitignore +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/.mise.toml +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/CLAUDE.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/CONTRIBUTING.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/GEMINI.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/LICENSE +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/README.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/DESIGN-ARCHIVE-INDEX.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-access-control-actor-guard.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-cross-repo-implementation-audit.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-demo-test-strategy.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-graphrefly-spec-design.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-serialization-memory-footprint.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-tier2-parity-nonlocal-forward-inner.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/archive/docs/SESSION-universal-reduction-layer.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/benchmarks/py-baseline.json +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/docs/ADAPTER-CONTRACT.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/docs/benchmark.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/docs/docs-guidance.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/docs/test-guidance.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/examples/README.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/examples/basic_counter.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/llms.txt +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/compat/__init__.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/compat/async_utils.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/cancellation.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/clock.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/guard.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/meta.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/runner.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/subgraph_locks.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/core/versioning.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/__init__.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/backoff.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/backpressure.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/checkpoint.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/composite.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/cron.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/data_structures.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/sources.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/tier1.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/extra/tier2.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/graph/__init__.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/integrations/__init__.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/memory.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/messaging.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/orchestration.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/src/graphrefly/py.typed +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/bench_core.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/conftest.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_adapter_contract.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_adapters_ingest.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_adapters_storage.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_backpressure.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_cascading_cache.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_concurrency.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_dynamic_node.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_edge_cases.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_composite.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_data_structures.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_sources_http.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_tier1.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_extra_tier2.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_measurement_adapters.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_patterns_ai.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_patterns_cqrs.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_patterns_memory.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_patterns_messaging.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_patterns_orchestration.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_perf_smoke.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_protocol.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_reactive_block_layout.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_reactive_layout.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_regressions.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_runner.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_smoke.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_sugar.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/tests/test_versioning.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/.gitignore +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/README.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/astro.config.mjs +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/content.config.ts +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/package.json +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/pnpm-lock.yaml +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/public/llms.txt +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/py-api-sidebar.mjs +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/scripts/gen_api_docs.py +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/scripts/sync-docs.mjs +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/components/GraphreflyHero.astro +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/components/Header.astro +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/components/MobileMenuFooter.astro +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/components/PyodidePlayground.tsx +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/components/Sidebar.astro +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/components/SiteTitle.astro +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/DeferWhen.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/DistillBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/EmitStrategy.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/Extraction.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/HttpBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/JitterMode.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/Message.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/MessageType.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/Messages.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/NodeActions.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/NodeFn.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/NodeImpl.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/NodeStatus.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/PipeOperator.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/PubSubHub.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/TokenBucket.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/Versioned.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/audit.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/batch.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/buffer.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/buffer_count.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/buffer_time.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/cached.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/combine.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/concat.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/concat_map.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/constant.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/debounce.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/delay.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/derived.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/distill.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/effect.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/element_at.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/emit_with_batch.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/empty.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/exhaust_map.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/exponential.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/fibonacci.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/filter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/find.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/first.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/first_value_from.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/flat_map.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/for_each.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_any.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_async_iter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_awaitable.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_cron.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_git_hook.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_http.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_iter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_mcp.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_timer.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_webhook.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/from_websocket.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/gate.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/index.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/interval.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/is_batching.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/last.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/linear.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/log_slice.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/map.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/merge.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/message_tier.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/never.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/node.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/of.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/operator.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/pairwise.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/pausable.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/pipe.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/producer.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/pubsub.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/race.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/rate_limiter.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/reactive_index.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/reactive_list.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/reactive_log.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/reactive_map.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/reduce.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/repeat.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/replay.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/rescue.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/retry.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/sample.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/scan.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/share.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/skip.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/start_with.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/state.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/subscribe.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/switch_map.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/take.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/take_until.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/take_while.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/tap.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/throttle.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/throw_error.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/timeout.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/to_array.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/to_list.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/to_sse.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/to_websocket.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/token_bucket.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/token_tracker.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/verifiable.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/window.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/window_count.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/window_time.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/with_breaker.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/with_latest_from.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/with_status.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/api/zip.md +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/index.mdx +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content/docs/lab/python.mdx +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/content.config.ts +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/env.d.ts +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/src/styles/custom.css +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/theme-prototypes.html +0 -0
- {graphrefly-0.6.0 → graphrefly-0.8.0}/website/tsconfig.json +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.8.0 (2026-04-06)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- 8.1 + django integration
|
|
10
|
+
([`5dcfd3d`](https://github.com/graphrefly/graphrefly-py/commit/5dcfd3d78561e7ae8cdc905f95f9b4922926881e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v0.7.0 (2026-04-06)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- Central timer util
|
|
18
|
+
([`d1285d5`](https://github.com/graphrefly/graphrefly-py/commit/d1285d5c4c95928b1c27e10ca77204ee0f8aaa5e))
|
|
19
|
+
|
|
20
|
+
- Redesign initial + cached
|
|
21
|
+
([`86d8506`](https://github.com/graphrefly/graphrefly-py/commit/86d850623e4e09fda1e8547ab67ca6e4cefb2a48))
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
## v0.6.0 (2026-04-05)
|
|
6
25
|
|
|
7
26
|
### Chores
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphrefly
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Reactive graph protocol for human + LLM co-operation. Composable nodes, glitch-free diamond resolution, two-phase push, durable streaming. Zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://py.graphrefly.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/graphrefly/graphrefly-py
|
|
@@ -126,24 +126,39 @@ Union-find over node identity merges components when nodes list dependencies at
|
|
|
126
126
|
- **~~Synchronous SQLite blocking in `to_sqlite` sink (Phase 5.2b, noted 2026-04-04, resolved 2026-04-04):~~** Both TS and PY now accept `batch_insert` / `batchInsert` (default `False`) and `max_batch_size` / `maxBatchSize` (default `1000`). DATA values are buffered and flushed inside a `BEGIN`/`COMMIT` transaction on terminal messages (`message_tier >= 3`), at `max_batch_size` threshold, or on `dispose()`. On insert error, the first error triggers `break` + `ROLLBACK`. BEGIN failure preserves pending data for retry via manual `flush()`. Both return `BufferedSinkHandle` when batch mode is enabled.
|
|
127
127
|
- **CheckpointAdapter refactored to key-value (Phase 3.1c, noted 2026-04-05, resolved 2026-04-05):** `CheckpointAdapter` changed from blob-based (`save(data)` / `load()`) to key-value (`save(key, data)` / `load(key)` / `clear(key)`). Unifies checkpoint persistence and cache storage under one interface. Both TS and PY updated. `save_graph_checkpoint` / `restore_graph_checkpoint` pass `graph.name` as key. `auto_checkpoint` passes `self.name` as key. `FileCheckpointAdapter` now takes a directory (one file per key, sanitized filenames). `DictCheckpointAdapter` no longer takes an internal key in constructor. `SqliteCheckpointAdapter` no longer takes a fixed key — caller provides it. Pre-1.0, all downstream consumers updated, no legacy shims.
|
|
128
128
|
- **`tiered_storage` uses `CheckpointAdapter[]` directly (Phase 3.1c, noted 2026-04-05, resolved 2026-04-05):** With key-value `CheckpointAdapter`, `tiered_storage` wraps adapters as `CacheTier`s naturally. No separate `CacheTier` interface needed for end users — `CheckpointAdapter` serves both checkpoint and cache use cases.
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
- **PY `SqliteCheckpointAdapter` default `check_same_thread=True` (Phase 3.1, noted 2026-04-05):** Python's `sqlite3.connect()` defaults to `check_same_thread=True`, raising `ProgrammingError` if `save()` is called from a timer/debounce thread while `load()` runs on the main thread.
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
129
|
+
- **~~`equals` must never see undefined/None (all phases, noted 2026-04-05, resolved 2026-04-05):~~** Superseded by `_SENTINEL` / `NO_VALUE` sentinel below. Original fix used `_has_emitted_data` flag; the sentinel approach replaced both.
|
|
130
|
+
- **~~`_cached is None` sentinel is ambiguous with `[(DATA, None)]` and INVALIDATE (noted 2026-04-05, resolved 2026-04-05):~~** Superseded by `_SENTINEL` / `NO_VALUE` sentinel below.
|
|
131
|
+
- **~~Derived node error observability gap (all phases, noted 2026-04-05, resolved 2026-04-05):~~** `fn`/`equals`/`on_message` errors now wrapped with `RuntimeError(f'Node "{name}": fn threw')` / `equals threw` / `on_message threw` with `__cause__`. Both TS and PY.
|
|
132
|
+
- **~~PY `retry` lock scope around `connect()` (Phase 3.1, noted 2026-04-05, resolved 2026-04-05):~~** Verified that `connect()` already runs outside the lock. Added clarifying comment.
|
|
133
|
+
- **PY `SqliteCheckpointAdapter` default `check_same_thread=True` (Phase 3.1, noted 2026-04-05):** Python's `sqlite3.connect()` defaults to `check_same_thread=True`, raising `ProgrammingError` if `save()` is called from a timer/debounce thread while `load()` runs on the main thread. `check_same_thread=False` + `threading.Lock` fix was prepared but reverted by linter. Deferred: re-apply when multi-threaded checkpoint usage becomes a pattern.
|
|
134
|
+
- **~~Payload-less DATA spec note (Phase 5.2–5.3, noted 2026-04-05, resolved 2026-04-05):~~** Spec clarified: payload-less DATA (`(DATA,)`) is not valid per protocol — all DATA tuples must carry a payload. Sink `msg[1] if len(msg) > 1 else None` coercion retained as defensive guard; upstream producers are the source of truth.
|
|
135
|
+
- **~~Sink `dispose()` error routing (Phase 5.2–5.3, noted 2026-04-05, resolved 2026-04-05):~~** All 20 PY sink `dispose()` functions now route errors to the `errors` companion node before suppressing.
|
|
136
|
+
- **~~`from_django_orm` and `from_tortoise` have identical implementations (Phase 5.2, noted 2026-04-05, resolved 2026-04-05):~~** Extracted `_from_sync_rows` helper. Both entry points kept with distinct validation.
|
|
137
|
+
- **~~Auto-edge registration from constructor deps (all phases, noted 2026-04-05, resolved 2026-04-05):~~** `Graph.add()` now auto-registers edges from constructor deps. Eliminates dual-bookkeeping bug surface.
|
|
138
|
+
- **~~ResettableTimer primitive (Phase 3.1, noted 2026-04-05, resolved 2026-04-05):~~** `ResettableTimer` in `core/timer.py`. `retry`, `rate_limiter`, `timeout` refactored to use it.
|
|
139
|
+
- **~~`to_json()` → `to_dict()` + `to_json_string()` (Phase 1.4, noted 2026-04-05, resolved 2026-04-05):~~** PY: `to_json()` renamed to `to_json_string()`; `to_dict()` added as alias of `snapshot()`; `to_json` alias removed (pre-1.0, no backward compat needed). TS: `toJSON()` renamed to `toObject()`; `toJSON()` kept as ECMAScript hook. Spec §3.8 updated.
|
|
140
|
+
- **~~Initial value, cached state, and equals interaction (all phases, noted 2026-04-05, resolved 2026-04-05):~~** Resolved with `_SENTINEL` / `NO_VALUE` sentinel (TS: `Symbol.for("graphrefly/NO_VALUE")`, PY: `_SENTINEL = object()`). Replaces the `_has_emitted_data` boolean flag entirely. One field (`_cached`) instead of two — impossible to desync. Key semantics: (1) When `initial` option is present (even as `None`), `_cached = initial` — `equals` IS called on first emission. (2) When `initial` is absent, `_cached = _SENTINEL` — first emission always DATA. (3) INVALIDATE / `reset_on_teardown` set `_cached = _SENTINEL`. (4) Resubscribable: terminal reset now also sets `_cached = _SENTINEL` — new subscriber always gets DATA. (5) Reconnect: cache retained → same-value emits RESOLVED — correct. (6) `get()` returns `None` when `_cached is _SENTINEL`. Spec §2.5 updated.
|
|
141
|
+
- **Auto-edge registration is local-only (Phase 1.1, noted 2026-04-05):** `Graph.add()` auto-registers edges for deps within the same `Graph` instance only. Cross-subgraph deps still require explicit `connect()`. Consistent with spec: cross-subgraph edges are explicit wiring.
|
|
142
|
+
- **Reduction primitives cross-language parity (Phase 8.1, noted 2026-04-06, QA 2026-04-06):** Both TS and PY implement `stratify`, `funnel`, `feedback`, `budget_gate`/`budgetGate`, and `scorer` in `patterns/reduction`. All five follow the orchestration factory pattern (`_base_meta`, `_register_step`). Key alignment: (1) `stratify` buffers DIRTY until DATA arrives — on classifier miss, emits `[DIRTY, RESOLVED]` to preserve spec §1.3.1 (both). (2) `funnel` bridges stages via `subscribe` forwarding DIRTY/DATA/RESOLVED/COMPLETE/ERROR to preserve two-phase protocol. TODO(8.2): replace with graph-visible bridge nodes. (3) `feedback` counter node is source of truth (resettable via `graph.set()`); uses `continue` (not `return`) on max_iterations so remaining batch messages process. Counter name is `__feedback_<condition>` to support multiple loops per graph. (4) `budget_gate`/`budgetGate` force-flushes all buffered items on terminal regardless of budget; sends RESUME before terminal if paused; forwards constraint ERROR downstream, silences constraint COMPLETE, forwards unknown constraint types via default. (5) `scorer` coerces `None`/`undefined` to 0 before multiplication (no TypeError/NaN divergence). TS `ScoredItem` is a plain object; PY `ScoredItem` is a class with `__slots__` and `__eq__`. Meta keys: `reduction: True`, `reduction_type: "<name>"`. Both repos: 22 tests each.
|
|
137
143
|
|
|
138
144
|
---
|
|
139
145
|
|
|
146
|
+
## Cross-language parity fixes (2026-04-05)
|
|
147
|
+
|
|
148
|
+
- **~~PY error wrapping missing original message (resolved 2026-04-05):~~** `RuntimeError(f'Node "{name}": fn threw')` omitted the original error message. TS included it: `fn threw: ${errMsg}`. Fixed all 3 PY sites (`fn threw`, `equals threw`, `on_message threw`) to include `{err}`. TS `onMessage` error was also not wrapped — now wraps as `Node "${name}": onMessage threw: ${errMsg}` with `{ cause }`.
|
|
149
|
+
- **~~PY `Graph.add()` missing reverse edge scan (resolved 2026-04-05):~~** PY only did forward auto-edge registration. TS did both forward + reverse (existing nodes whose deps include the new node). Fixed: PY now does the reverse scan, matching TS.
|
|
150
|
+
- **~~PY `_emit_sequential` terminal routing (resolved 2026-04-05):~~** PY appended terminals to the same queue as DATA/RESOLVED. TS routed them to `pendingPhase3`. Fixed: PY terminals now go to `bs.pending_phase3`.
|
|
151
|
+
- **~~PY `emit_with_batch` default strategy (resolved 2026-04-05):~~** PY defaulted to `"sequential"`, TS to `"partition"`. Fixed: PY default changed to `"partition"`.
|
|
152
|
+
- **~~PY `retry` sink missing `stopped` guard (resolved 2026-04-05):~~** TS checked `if (stopped) return;` at top of retry subscribe callback. PY had no such guard. Fixed: added `if stopped[0]: return`.
|
|
153
|
+
- **~~PY `fallback` plain-value path used `down()` (resolved 2026-04-05):~~** TS used `a.emit(fb)` (goes through equals + DIRTY/DATA wrapping). PY used `actions.down([(DATA, fb), (COMPLETE,)])` (bypassed equals). Fixed: PY now uses `actions.emit(fb)` then `actions.down([(COMPLETE,)])`.
|
|
154
|
+
|
|
140
155
|
## Cross-language implementation notes
|
|
141
156
|
|
|
142
157
|
**Keep this section in sync with `graphrefly-ts/docs/optimizations.md` § Cross-language implementation notes** so you can open both files side by side.
|
|
143
158
|
|
|
144
|
-
-
|
|
159
|
+
- **~~Dual-bookkeeping of derived deps + connect edges (all phases, noted 2026-04-05, resolved 2026-04-05):~~** `Graph.add()` now auto-registers edges from constructor deps in both TS and PY. Eliminates the divergence risk between node-level deps and graph-level edge registry. All graph factories no longer need explicit `connect()` calls for constructor deps.
|
|
145
160
|
|
|
146
|
-
- **Resilience composition parity (Phase 3.1c, 2026-04-05):** Both TS and PY implement `fallback(source, fb)`, `timeout(source, timeoutNs/timeout_ns)`, `cache(source, ttlNs/ttl_ns)`, `cascadingCache`/`cascading_cache`, and `tieredStorage`/`tiered_storage`. `CacheEvictionPolicy`/`EvictionPolicy` use aligned method names: `insert`/`touch`/`delete`/`evict(count)`/`size()`. `CacheTier`: `load` is required; `save`/`clear` are optional (TS `?` modifier; PY uses `hasattr` checks). `tieredStorage`/`tiered_storage` returns a wrapper with `.cache` property exposing the inner `CascadingCache` in both languages. Eviction demotes to deepest tier with `save` before removing — value is preserved in cold storage. Cache miss sentinel: TS `undefined`, PY `None`. `TimeoutError`: TS extends `Error`; PY subclasses `builtins.TimeoutError` so `except TimeoutError` catches both. Validation: TS `RangeError`, PY `ValueError` — language convention. `cache()` replay emits raw `DATA` (no DIRTY/RESOLVED) on both sides. §5.10 timer exception: `timeout`, `retry`, `rateLimiter`/`rate_limiter`
|
|
161
|
+
- **Resilience composition parity (Phase 3.1c, 2026-04-05):** Both TS and PY implement `fallback(source, fb)`, `timeout(source, timeoutNs/timeout_ns)`, `cache(source, ttlNs/ttl_ns)`, `cascadingCache`/`cascading_cache`, and `tieredStorage`/`tiered_storage`. `CacheEvictionPolicy`/`EvictionPolicy` use aligned method names: `insert`/`touch`/`delete`/`evict(count)`/`size()`. `CacheTier`: `load` is required; `save`/`clear` are optional (TS `?` modifier; PY uses `hasattr` checks). `tieredStorage`/`tiered_storage` returns a wrapper with `.cache` property exposing the inner `CascadingCache` in both languages. Eviction demotes to deepest tier with `save` before removing — value is preserved in cold storage. Cache miss sentinel: TS `undefined`, PY `None`. `TimeoutError`: TS extends `Error`; PY subclasses `builtins.TimeoutError` so `except TimeoutError` catches both. Validation: TS `RangeError`, PY `ValueError` — language convention. `cache()` replay emits raw `DATA` (no DIRTY/RESOLVED) on both sides. §5.10 timer exception: `timeout`, `retry`, `rateLimiter`/`rate_limiter` now use `ResettableTimer` (`core/timer.py` in PY, `core/timer.ts` in TS) — a reusable primitive extracted from the repeated `setTimeout`/`threading.Timer` pattern.
|
|
147
162
|
- **Progressive disclosure parity (Phase 3.3b, 2026-04-04):** Both TS and PY `describe()` support `detail` (`"minimal"` / `"standard"` / `"full"`) and `fields` (GraphQL-style field selection with dotted meta paths like `"meta.label"`). Default changed from returning all fields to `"minimal"` (type + deps only) in both languages — pre-1.0, no backward compat concern. `format: "spec"` / `format="spec"` forces minimal fields. Both return an `expand()` method on the result for re-reading with higher detail. TS `expand` is a property on the result object; PY `describe()` returns a `DescribeResult` (dict subclass) with `expand()` as a method — `json.dumps(graph.describe())` works safely in both languages (TS `JSON.stringify` drops functions; PY `expand` is not a dict key). `observe()` supports `detail` with the same three levels: `"minimal"` (DATA events only in events list, counts still tracked), `"standard"` (current behavior), `"full"` (implies structured + timeline + causal + derived). `ObserveResult` has `expand()` in both languages. TS `expand` is a method on the result object; PY `expand` is a method on the `ObserveResult` dataclass. Internal callers (`snapshot`, `auto_checkpoint`, `dump_graph`, AI pattern functions) explicitly pass the detail level they need. Diagram methods (`to_mermaid`/`toMermaid`, `to_d2`/`toD2`) only use paths/edges so minimal default is fine — intentional, no detail option needed. `"standard"` includes versioning (`v`); `"full"` adds `guard` and `last_mutation`/`lastMutation` (runtime attribution, not restored by `restore()`). `snapshot()` / `auto_checkpoint` strip `last_mutation`/`guard` from persisted nodes so snapshot → restore → snapshot is idempotent — use `describe(detail="full")` for audit snapshots that include attribution. Unrecognized `detail` strings silently fall back to `"minimal"` — no runtime validation; we'll revisit if real-world usage shows this causes confusion. Dict/object filters (`meta_has`, `status`, etc.) operate on whatever fields the chosen detail level provides; at `"minimal"`, fields like `meta` and `status` are absent, so filters that depend on them silently exclude all nodes. Users should pass `detail="standard"` or higher when using these filters. Spec Appendix B `status` field changed from `required` to optional (schema applies at `detail >= "standard"`).
|
|
148
163
|
- **SQLite adapter parity (Phase 5.2b, 2026-04-04):** Both TS and PY use duck-typed `SqliteDbLike` with a `query(sql, params)` method — matching the `PostgresClientLike`/`ClickHouseClientLike` convention. TS `SqliteDbLike.query()` returns `unknown[]`; PY `SqliteDbLike.query()` returns `list[Any]`. Both are fully synchronous (no Promises/async). `from_sqlite`/`fromSqlite` is one-shot (DATA per row, then COMPLETE); compose with `switch_map` + `from_timer` for periodic re-query. `to_sqlite`/`toSqlite` follows per-record sink pattern (same as `to_postgres`/`toPostgres`). Default insert SQL uses JSON column; custom `to_sql` override available. TS uses `node:sqlite` `DatabaseSync` or `better-sqlite3`; PY uses stdlib `sqlite3` — both zero-dep from GraphReFly's perspective (user provides instance).
|
|
149
164
|
- **Storage & sink adapter pattern parity (Phase 5.2d, 2026-04-04):** All 5.2d sinks follow the same pattern in both TS and PY: duck-typed client protocols, `on_message` intercepting `DATA`, `SinkTransportError` for serialize/send failures. All sinks return a `SinkHandle` with `dispose()` + `errors: Node[SinkTransportError | None]`. `dispose()` sends `TEARDOWN` to the errors node. Buffered sinks (`to_clickhouse`, `to_s3`, `to_file`, `to_csv`) return a `BufferedSinkHandle` adding `flush()`. TS `SinkHandle` has optional `flush?: ...`; PY uses separate `BufferedSinkHandle` dataclass (intentional — more Pythonic). Checkpoint adapters (`checkpoint_to_s3`, `checkpoint_to_redis`) wire `graph.auto_checkpoint()`. PY uses `threading.Timer` for flush timers; TS uses `setTimeout`. PY `to_postgres` calls `client.execute(sql, params)` (psycopg2/3 style); TS calls `client.query(sql, params)` (pg style). PY `json.dumps` includes spaces after separators; TS `JSON.stringify` does not — NDJSON output is semantically equivalent but not byte-identical across languages.
|
|
@@ -312,14 +327,14 @@ Cross-language: `graphrefly-ts/docs/optimizations.md` §15. **Python (shipped):*
|
|
|
312
327
|
| **`snapshot()`** | Both: `{ version: 1, ...describe() }` — flat `version` field, sorted `nodes` keys. |
|
|
313
328
|
| **`restore(data)`** | Both: validate `data.name` matches graph name; skip `derived`/`operator`/`effect` types; silently ignore unknown/failing paths. |
|
|
314
329
|
| **`from_snapshot(data, build?)`** | Both: optional `build` callback registers topology before `restore()` applies values. Without `build`, both use registry-based reconstruction (mounts → topo node creation via factories → edges → restore). |
|
|
315
|
-
| **`
|
|
316
|
-
| **`toJSONString()`** | TS only — `JSON.stringify(toJSON()) + "\n"`. Python's `
|
|
330
|
+
| **`to_json_string()` / `toJSON()`** | Python `to_json_string()` returns compact JSON **string** with trailing newline. TS `toJSON()` returns a plain sorted-key **object** (for `JSON.stringify(graph)`). Language-appropriate. |
|
|
331
|
+
| **`toJSONString()`** | TS only — `JSON.stringify(toJSON()) + "\n"`. Python's `to_json_string()` serves the same role. |
|
|
317
332
|
|
|
318
333
|
**Intentional divergence:**
|
|
319
334
|
|
|
320
335
|
| Topic | Python | TypeScript | Rationale |
|
|
321
336
|
|-------|--------|------------|-----------|
|
|
322
|
-
| `
|
|
337
|
+
| `to_json_string` return type | `to_json_string()` → `str` (no universal `__json__` hook in Python) | `toJSON()` → plain object (ECMAScript `JSON.stringify` protocol) | Language idiom |
|
|
323
338
|
| `_parse_snapshot_envelope` | Validates `version`, `name`, `nodes`, `edges`, `subgraphs` types | Only validates `data.name` match | Python is stricter; both correct |
|
|
324
339
|
|
|
325
340
|
### Ingest adapters (roadmap 5.2c / 5.3b) — deferred items (QA)
|
|
@@ -330,7 +345,7 @@ Applies to `src/extra/adapters.ts` and `graphrefly.extra.adapters`. **Keep the t
|
|
|
330
345
|
|------|--------|-------|
|
|
331
346
|
| **`fromRedisStream` / `from_redis_stream` never emits COMPLETE** | Documented limitation (2026-04-03) | Long-lived stream consumers intentionally never complete. The consumer loop runs until teardown. This is expected behavior for persistent stream sources (same as Kafka). Document in JSDoc/docstrings. |
|
|
332
347
|
| **`fromRedisStream` / `from_redis_stream` does not disconnect client** | Documented limitation (2026-04-03) | The caller owns the Redis client lifecycle. The adapter does not call `disconnect()` on teardown — the caller is responsible for closing the connection. Same contract as `fromKafka` (caller owns `consumer.connect()`/`disconnect()`). |
|
|
333
|
-
| **PY `from_csv` / `from_ndjson` thread
|
|
348
|
+
| **PY `from_csv` / `from_ndjson` thread cleanup** | Resolved (2026-04-05) | `t.join(timeout=1)` added after stop flag. |
|
|
334
349
|
|
|
335
350
|
### Ingest adapters — intentional cross-language divergences (parity review 2026-04-03)
|
|
336
351
|
|
|
@@ -377,10 +392,10 @@ Applies to `src/extra/adapters.ts` and `graphrefly.extra.adapters`. **Keep the t
|
|
|
377
392
|
| Node internals | Class-based `NodeImpl`, all methods on class | Class-based `NodeImpl`, V8 hidden class optimization, prototype methods |
|
|
378
393
|
| Dep-value identity check | Before cleanup (skip cleanup+fn on no-op) | Before cleanup (skip cleanup+fn on no-op) |
|
|
379
394
|
| `INVALIDATE` (§1.2) | Cleanup + clear `_cached` + `_last_dep_values`; terminal passthrough (§9); no auto recompute | Same |
|
|
380
|
-
| `Graph` Phase 1.1 | `thread_safe` + `RLock`; TEARDOWN after unlock on `remove`; `disconnect` registry-only (§C resolved) | Registry only; `connect` / `disconnect` errors aligned; §C resolved |
|
|
395
|
+
| `Graph` Phase 1.1 | `thread_safe` + `RLock`; TEARDOWN after unlock on `remove`; `disconnect` registry-only (§C resolved); `add()` auto-registers edges from constructor deps | Registry only; `connect` / `disconnect` errors aligned; §C resolved; `add()` auto-registers edges from constructor deps |
|
|
381
396
|
| `Graph` Phase 1.2 | Aligned: `::` path separator, mount `remove` + subtree TEARDOWN, qualified paths, `edges()`, signal mounts-first, `resolve` strips leading name, `:` in names OK; see §14 | Same; see §14 |
|
|
382
397
|
| `Graph` Phase 1.3 | `describe`, `observe`, `GRAPH_META_SEGMENT`, `signal`→meta, `describe_kind` on sugar; see §15 | TS: `describe()`, `observe()`, `GRAPH_META_SEGMENT`, `describeKind` on sugar; see graphrefly-ts §15 | `observe()` order: both use full-path code-point sort (resolved 2026-03-31; see §15) |
|
|
383
|
-
| `Graph` Phase 1.4 | `destroy`, `snapshot` (flat `version: 1`), `restore` (name check + type filter + silent catch), `from_snapshot(data, build=)`, `
|
|
398
|
+
| `Graph` Phase 1.4 | `destroy`, `snapshot` (flat `version: 1`), `restore` (name check + type filter + silent catch), `from_snapshot(data, build=)`, `to_json_string()` → str + `\n`; see §16 | `destroy`, `snapshot`, `restore`, `fromSnapshot(data, build?)`, `toJSON()` → object, `toJSONString()` → str + `\n`; see §16 |
|
|
384
399
|
| `Graph` Phase 1.5 | **Python:** `Actor`, `GuardDenied`, `policy()`, `compose_guards`, node `guard` opt, `down`/`set`/`signal`/`subscribe`/`describe` actor params, `internal` propagation bypass, `remove`/unmount subtree TEARDOWN `internal=True`; see built-in §8 | **TypeScript:** aligned — `GraphActorOptions`, `NodeTransportOptions`, scoped `describe`/`observe`, `GuardDenied.node` getter mirrors `nodeName` |
|
|
385
400
|
| `policy()` semantics | Deny-overrides: any matching deny blocks; if no deny, any matching allow permits; no match → deny | Same (aligned from parity round) |
|
|
386
401
|
| `DEFAULT_ACTOR` | `{"type": "system", "id": ""}` | `{ type: "system", id: "" }` (aligned) |
|
|
@@ -399,7 +414,7 @@ Applies to `src/extra/adapters.ts` and `graphrefly.extra.adapters`. **Keep the t
|
|
|
399
414
|
| `from_event_emitter` / `fromEvent` | Generic emitter (`add_method=`, `remove_method=`) | DOM `addEventListener` API |
|
|
400
415
|
| `to_array` / `toArray` | Reactive `Node[list]` | Reactive `Node<T[]>` |
|
|
401
416
|
| `to_list` (blocking) | Py-only sync bridge | N/A |
|
|
402
|
-
| Extra Phase 3.1 (resilience) | `graphrefly.extra.{backoff,resilience,checkpoint}
|
|
417
|
+
| Extra Phase 3.1 (resilience) | `graphrefly.extra.{backoff,resilience,checkpoint}` + `core/timer.py` (`ResettableTimer`); see §6 below | `src/extra/{backoff,resilience,checkpoint}.ts` + `core/timer.ts` (`ResettableTimer`); see §6 below |
|
|
403
418
|
| Extra Phase 3.2 (data structures) | `graphrefly.extra.data_structures` (`reactive_map`, …); see §17 | `reactiveMap` + `reactive-base` (`Versioned` snapshots); see §17 |
|
|
404
419
|
|
|
405
420
|
### 18. CQRS reactive log snapshot shape (Phase 4.5 — cross-language note)
|
|
@@ -535,7 +550,7 @@ Both repos now ship a Pulsar-inspired messaging domain layer under `patterns.mes
|
|
|
535
550
|
|
|
536
551
|
| Topic | Python | TypeScript | Rationale |
|
|
537
552
|
|-------|--------|------------|-----------|
|
|
538
|
-
| Timer base | `monotonic_ns()` (nanoseconds via `time.monotonic_ns()`) | `monotonicNs()` (nanoseconds via `performance.now()`) | Both centralised in `core/clock`; nanosecond internal tracking |
|
|
553
|
+
| Timer base | `monotonic_ns()` (nanoseconds via `time.monotonic_ns()`); `ResettableTimer` in `core/timer.py` | `monotonicNs()` (nanoseconds via `performance.now()`); `ResettableTimer` in `core/timer.ts` | Both centralised in `core/clock`; nanosecond internal tracking; `ResettableTimer` used by `retry`, `rate_limiter`, `timeout` |
|
|
539
554
|
| Thread safety | `CircuitBreaker` + `TokenBucket` use `threading.Lock`; retry uses `threading.Timer` | Single-threaded (`setTimeout`) | Spec §6.1 |
|
|
540
555
|
| `CircuitBreaker` params | `cooldown` (seconds, implicit) | `cooldownSeconds` (seconds, explicit) | Naming convention |
|
|
541
556
|
| `CircuitOpenError` base | `RuntimeError` | `Error` | Language convention |
|
|
@@ -293,7 +293,7 @@ Composition layer over 3.2 (`reactive_log`), 4.1 (sagas), 4.2 (event bus), 4.3 (
|
|
|
293
293
|
### 5.1 — Framework compat
|
|
294
294
|
|
|
295
295
|
- [x] FastAPI integration
|
|
296
|
-
- [
|
|
296
|
+
- [x] Django integration
|
|
297
297
|
- [x] asyncio / trio Runner protocol
|
|
298
298
|
- [x] Async utilities: `to_async_iter`, `first_value_from_async`, `settled`
|
|
299
299
|
|
|
@@ -487,11 +487,11 @@ Reusable patterns for taking heterogeneous massive inputs and producing prioriti
|
|
|
487
487
|
|
|
488
488
|
Composable building blocks between sources and sinks.
|
|
489
489
|
|
|
490
|
-
- [
|
|
491
|
-
- [
|
|
492
|
-
- [
|
|
493
|
-
- [
|
|
494
|
-
- [
|
|
490
|
+
- [x] `stratify(source, rules)` → Graph — route input to different reduction branches based on classifier fn. Each branch gets independent operator chains. Rules are reactive — an LLM can rewrite them at runtime.
|
|
491
|
+
- [x] `funnel(sources, stages)` → Graph — multi-source merge with sequential reduction stages. Each stage is a named subgraph. Stages are pluggable — swap a stage by graph composition.
|
|
492
|
+
- [x] `feedback(graph, condition, reentry)` → Graph — introduce a cycle: when condition node fires, route output back to reentry point. Bounded by max iterations + budget constraints.
|
|
493
|
+
- [x] `budget_gate(source, constraints)` → Node — pass-through respecting reactive constraint nodes (token budget, network IO, cost ceiling). Backpressure via PAUSE/RESUME.
|
|
494
|
+
- [x] `scorer(sources, weights)` → Node — reactive multi-signal scoring. Weights are nodes (LLM or human can adjust live). Output: sorted, prioritized items with full score breakdown in meta.
|
|
495
495
|
|
|
496
496
|
### 8.2 — Domain templates (opinionated Graph factories)
|
|
497
497
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "graphrefly"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Reactive graph protocol for human + LLM co-operation. Composable nodes, glitch-free diamond resolution, two-phase push, durable streaming. Zero dependencies."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -60,6 +60,7 @@ dev = [
|
|
|
60
60
|
"ruff>=0.9",
|
|
61
61
|
"mypy>=1.14",
|
|
62
62
|
"croniter>=2.0",
|
|
63
|
+
"django>=4.2",
|
|
63
64
|
"fastapi>=0.100",
|
|
64
65
|
"httpx>=0.24",
|
|
65
66
|
"pillow>=12.2.0",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from graphrefly import compat, integrations, patterns
|
|
4
4
|
from graphrefly.core import (
|
|
5
|
+
NO_VALUE,
|
|
5
6
|
V0,
|
|
6
7
|
V1,
|
|
7
8
|
Actor,
|
|
@@ -115,6 +116,7 @@ __all__ = [
|
|
|
115
116
|
"Message",
|
|
116
117
|
"MessageType",
|
|
117
118
|
"Messages",
|
|
119
|
+
"NO_VALUE",
|
|
118
120
|
"Node",
|
|
119
121
|
"PipeOperator",
|
|
120
122
|
"NodeActions",
|
|
@@ -18,6 +18,7 @@ from graphrefly.core.guard import (
|
|
|
18
18
|
)
|
|
19
19
|
from graphrefly.core.meta import describe_node, meta_snapshot
|
|
20
20
|
from graphrefly.core.node import (
|
|
21
|
+
NO_VALUE,
|
|
21
22
|
Node,
|
|
22
23
|
NodeActions,
|
|
23
24
|
NodeFn,
|
|
@@ -64,6 +65,7 @@ from graphrefly.core.sugar import (
|
|
|
64
65
|
producer,
|
|
65
66
|
state,
|
|
66
67
|
)
|
|
68
|
+
from graphrefly.core.timer import ResettableTimer
|
|
67
69
|
from graphrefly.core.versioning import (
|
|
68
70
|
V0,
|
|
69
71
|
V1,
|
|
@@ -87,6 +89,7 @@ __all__ = [
|
|
|
87
89
|
"GuardDenied",
|
|
88
90
|
"GuardFn",
|
|
89
91
|
"Message",
|
|
92
|
+
"NO_VALUE",
|
|
90
93
|
"MessageType",
|
|
91
94
|
"Messages",
|
|
92
95
|
"Node",
|
|
@@ -95,6 +98,7 @@ __all__ = [
|
|
|
95
98
|
"NodeFn",
|
|
96
99
|
"NodeImpl",
|
|
97
100
|
"NodeStatus",
|
|
101
|
+
"ResettableTimer",
|
|
98
102
|
"SubscribeHints",
|
|
99
103
|
"monotonic_ns",
|
|
100
104
|
"wall_clock_ns",
|
|
@@ -13,8 +13,12 @@ import threading
|
|
|
13
13
|
from collections.abc import Callable, Mapping
|
|
14
14
|
from contextlib import suppress
|
|
15
15
|
from types import MappingProxyType
|
|
16
|
-
from typing import Any
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from graphrefly.core.guard import MutationRecord
|
|
20
|
+
|
|
21
|
+
from graphrefly.core.node import _SENTINEL
|
|
18
22
|
from graphrefly.core.protocol import Messages, MessageType, emit_with_batch, propagates_to_meta
|
|
19
23
|
|
|
20
24
|
# ---------------------------------------------------------------------------
|
|
@@ -183,13 +187,13 @@ class DynamicNodeImpl[T]:
|
|
|
183
187
|
self._on_resubscribe = on_resubscribe
|
|
184
188
|
self._auto_complete = complete_when_deps_complete
|
|
185
189
|
self._describe_kind = describe_kind
|
|
186
|
-
self._last_mutation:
|
|
190
|
+
self._last_mutation: MutationRecord | None = None
|
|
187
191
|
self._resubscribable = resubscribable
|
|
188
192
|
self._reset_on_teardown = reset_on_teardown
|
|
189
193
|
self._thread_safe = bool(thread_safe)
|
|
190
194
|
self._inspector_hook: Callable[[dict[str, Any]], None] | None = None
|
|
191
195
|
|
|
192
|
-
self._cached:
|
|
196
|
+
self._cached: Any = _SENTINEL
|
|
193
197
|
self._status: str = "disconnected"
|
|
194
198
|
self._terminal = False
|
|
195
199
|
self._connected = False
|
|
@@ -254,7 +258,7 @@ class DynamicNodeImpl[T]:
|
|
|
254
258
|
return self._meta
|
|
255
259
|
|
|
256
260
|
@property
|
|
257
|
-
def last_mutation(self) ->
|
|
261
|
+
def last_mutation(self) -> MutationRecord | None:
|
|
258
262
|
return self._last_mutation
|
|
259
263
|
|
|
260
264
|
@property
|
|
@@ -277,8 +281,10 @@ class DynamicNodeImpl[T]:
|
|
|
277
281
|
lock = self._cache_lock
|
|
278
282
|
if lock is not None:
|
|
279
283
|
with lock:
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
v = self._cached
|
|
285
|
+
else:
|
|
286
|
+
v = self._cached
|
|
287
|
+
return None if v is _SENTINEL else v
|
|
282
288
|
|
|
283
289
|
def down(
|
|
284
290
|
self,
|
|
@@ -346,6 +352,12 @@ class DynamicNodeImpl[T]:
|
|
|
346
352
|
) -> Callable[[], None]:
|
|
347
353
|
if self._terminal and self._resubscribable:
|
|
348
354
|
self._terminal = False
|
|
355
|
+
lock = self._cache_lock
|
|
356
|
+
if lock is not None:
|
|
357
|
+
with lock:
|
|
358
|
+
self._cached = _SENTINEL
|
|
359
|
+
else:
|
|
360
|
+
self._cached = _SENTINEL
|
|
349
361
|
self._status = "disconnected"
|
|
350
362
|
if self._on_resubscribe is not None:
|
|
351
363
|
self._on_resubscribe()
|
|
@@ -519,11 +531,14 @@ class DynamicNodeImpl[T]:
|
|
|
519
531
|
if t is MessageType.INVALIDATE:
|
|
520
532
|
if lock is not None:
|
|
521
533
|
with lock:
|
|
522
|
-
self._cached =
|
|
534
|
+
self._cached = _SENTINEL
|
|
523
535
|
else:
|
|
524
|
-
self._cached =
|
|
525
|
-
|
|
536
|
+
self._cached = _SENTINEL
|
|
537
|
+
self._status = "dirty"
|
|
538
|
+
if t is MessageType.DATA:
|
|
526
539
|
self._status = "settled"
|
|
540
|
+
elif t is MessageType.RESOLVED:
|
|
541
|
+
self._status = "resolved"
|
|
527
542
|
elif t is MessageType.DIRTY:
|
|
528
543
|
self._status = "dirty"
|
|
529
544
|
elif t is MessageType.COMPLETE:
|
|
@@ -536,9 +551,9 @@ class DynamicNodeImpl[T]:
|
|
|
536
551
|
if self._reset_on_teardown:
|
|
537
552
|
if lock is not None:
|
|
538
553
|
with lock:
|
|
539
|
-
self._cached =
|
|
554
|
+
self._cached = _SENTINEL
|
|
540
555
|
else:
|
|
541
|
-
self._cached =
|
|
556
|
+
self._cached = _SENTINEL
|
|
542
557
|
try:
|
|
543
558
|
self._propagate_to_meta(t)
|
|
544
559
|
finally:
|
|
@@ -561,7 +576,13 @@ class DynamicNodeImpl[T]:
|
|
|
561
576
|
cached_snapshot = self._cached
|
|
562
577
|
else:
|
|
563
578
|
cached_snapshot = self._cached
|
|
564
|
-
|
|
579
|
+
try:
|
|
580
|
+
unchanged = cached_snapshot is not _SENTINEL and self._equals(cached_snapshot, value)
|
|
581
|
+
except Exception as eq_err:
|
|
582
|
+
wrapped = RuntimeError(f'Node "{self._name}": equals threw: {eq_err}')
|
|
583
|
+
wrapped.__cause__ = eq_err
|
|
584
|
+
self._down_internal([(MessageType.ERROR, wrapped)])
|
|
585
|
+
return
|
|
565
586
|
if unchanged:
|
|
566
587
|
msgs: Messages = (
|
|
567
588
|
[(MessageType.RESOLVED,)]
|
|
@@ -14,6 +14,7 @@ from graphrefly.core.guard import (
|
|
|
14
14
|
Actor,
|
|
15
15
|
GuardAction,
|
|
16
16
|
GuardDenied,
|
|
17
|
+
MutationRecord,
|
|
17
18
|
normalize_actor,
|
|
18
19
|
record_mutation,
|
|
19
20
|
)
|
|
@@ -32,6 +33,12 @@ from graphrefly.core.versioning import (
|
|
|
32
33
|
default_hash,
|
|
33
34
|
)
|
|
34
35
|
|
|
36
|
+
# Internal sentinel: "no cached value has been set or emitted."
|
|
37
|
+
# Distinct from None so that None can be a valid emitted value.
|
|
38
|
+
_SENTINEL = object()
|
|
39
|
+
|
|
40
|
+
NO_VALUE = _SENTINEL
|
|
41
|
+
|
|
35
42
|
# --- Status & typing (graphrefly-ts node.ts) ---------------------------------
|
|
36
43
|
|
|
37
44
|
type NodeStatus = str # structural: same strings as TS NodeStatus
|
|
@@ -203,6 +210,7 @@ class NodeImpl[T]:
|
|
|
203
210
|
"_meta",
|
|
204
211
|
"_name",
|
|
205
212
|
"_on_message",
|
|
213
|
+
"_on_resubscribe",
|
|
206
214
|
"_opts",
|
|
207
215
|
"_producer_started",
|
|
208
216
|
"_resubscribable",
|
|
@@ -236,6 +244,7 @@ class NodeImpl[T]:
|
|
|
236
244
|
self._thread_safe: bool = bool(opts.get("thread_safe", True))
|
|
237
245
|
|
|
238
246
|
self._on_message = opts.get("on_message")
|
|
247
|
+
self._on_resubscribe: Callable[[], None] | None = opts.get("on_resubscribe")
|
|
239
248
|
self._fn = fn
|
|
240
249
|
self._deps = deps
|
|
241
250
|
self._has_deps = len(deps) > 0
|
|
@@ -245,10 +254,10 @@ class NodeImpl[T]:
|
|
|
245
254
|
msg = "node option 'guard' must be callable or None"
|
|
246
255
|
raise TypeError(msg)
|
|
247
256
|
self._guard: Callable[[Actor, GuardAction], bool] | None = raw_guard
|
|
248
|
-
self._last_mutation:
|
|
257
|
+
self._last_mutation: MutationRecord | None = None
|
|
249
258
|
|
|
250
259
|
self._cache_lock = threading.Lock() if self._thread_safe else None
|
|
251
|
-
self._cached:
|
|
260
|
+
self._cached: Any = opts.get("initial", _SENTINEL)
|
|
252
261
|
self._status: NodeStatus = "disconnected" if self._has_deps else "settled"
|
|
253
262
|
|
|
254
263
|
# Versioning (GRAPHREFLY-SPEC §7)
|
|
@@ -257,7 +266,7 @@ class NodeImpl[T]:
|
|
|
257
266
|
self._versioning: NodeVersionInfo | None = (
|
|
258
267
|
create_versioning(
|
|
259
268
|
versioning_level,
|
|
260
|
-
self._cached,
|
|
269
|
+
None if self._cached is _SENTINEL else self._cached,
|
|
261
270
|
id=opts.get("versioning_id"),
|
|
262
271
|
hash_fn=self._hash_fn,
|
|
263
272
|
)
|
|
@@ -359,7 +368,7 @@ class NodeImpl[T]:
|
|
|
359
368
|
else:
|
|
360
369
|
self._cached = m[1] # type: ignore[misc]
|
|
361
370
|
if self._versioning is not None:
|
|
362
|
-
advance_version(self._versioning, m[1], self._hash_fn)
|
|
371
|
+
advance_version(self._versioning, m[1], self._hash_fn) # type: ignore[misc]
|
|
363
372
|
if t is MessageType.INVALIDATE:
|
|
364
373
|
# GRAPHREFLY-SPEC §1.2: clear cached state; do not auto-emit from here.
|
|
365
374
|
if self._cleanup is not None:
|
|
@@ -368,9 +377,9 @@ class NodeImpl[T]:
|
|
|
368
377
|
cb()
|
|
369
378
|
if lock is not None:
|
|
370
379
|
with lock:
|
|
371
|
-
self._cached =
|
|
380
|
+
self._cached = _SENTINEL
|
|
372
381
|
else:
|
|
373
|
-
self._cached =
|
|
382
|
+
self._cached = _SENTINEL
|
|
374
383
|
self._last_dep_values = None
|
|
375
384
|
self._status = _status_after_message(self._status, m)
|
|
376
385
|
if t is MessageType.COMPLETE or t is MessageType.ERROR:
|
|
@@ -379,9 +388,9 @@ class NodeImpl[T]:
|
|
|
379
388
|
if self._reset_on_teardown:
|
|
380
389
|
if lock is not None:
|
|
381
390
|
with lock:
|
|
382
|
-
self._cached =
|
|
391
|
+
self._cached = _SENTINEL
|
|
383
392
|
else:
|
|
384
|
-
self._cached =
|
|
393
|
+
self._cached = _SENTINEL
|
|
385
394
|
# Invoke cleanup for compute nodes (deps+fn) — spec §2.4
|
|
386
395
|
if self._cleanup is not None:
|
|
387
396
|
cb = self._cleanup
|
|
@@ -416,7 +425,16 @@ class NodeImpl[T]:
|
|
|
416
425
|
cached_snapshot = self._cached
|
|
417
426
|
else:
|
|
418
427
|
cached_snapshot = self._cached
|
|
419
|
-
|
|
428
|
+
# §2.5: equals() only compares two real DATA values. _SENTINEL
|
|
429
|
+
# disambiguates "never emitted" from "emitted None" and
|
|
430
|
+
# "reset via INVALIDATE/reset_on_teardown".
|
|
431
|
+
try:
|
|
432
|
+
unchanged = cached_snapshot is not _SENTINEL and self._equals(cached_snapshot, value)
|
|
433
|
+
except Exception as eq_err:
|
|
434
|
+
wrapped = RuntimeError(f'Node "{self._name}": equals threw: {eq_err}')
|
|
435
|
+
wrapped.__cause__ = eq_err
|
|
436
|
+
self.down([(MessageType.ERROR, wrapped)], internal=True)
|
|
437
|
+
return
|
|
420
438
|
if unchanged:
|
|
421
439
|
if was_dirty:
|
|
422
440
|
self.down([(MessageType.RESOLVED,)], internal=True)
|
|
@@ -470,7 +488,9 @@ class NodeImpl[T]:
|
|
|
470
488
|
return
|
|
471
489
|
self._emit_auto_value(out)
|
|
472
490
|
except Exception as err:
|
|
473
|
-
self.
|
|
491
|
+
wrapped = RuntimeError(f'Node "{self._name}": fn threw: {err}')
|
|
492
|
+
wrapped.__cause__ = err
|
|
493
|
+
self.down([(MessageType.ERROR, wrapped)], internal=True)
|
|
474
494
|
|
|
475
495
|
def _run_fn(self) -> None:
|
|
476
496
|
if self._fn is None:
|
|
@@ -519,7 +539,9 @@ class NodeImpl[T]:
|
|
|
519
539
|
if self._on_message(msg, index, self._actions):
|
|
520
540
|
continue
|
|
521
541
|
except Exception as err:
|
|
522
|
-
self.
|
|
542
|
+
wrapped = RuntimeError(f'Node "{self._name}": on_message threw: {err}')
|
|
543
|
+
wrapped.__cause__ = err
|
|
544
|
+
self.down([(MessageType.ERROR, wrapped)], internal=True)
|
|
523
545
|
return
|
|
524
546
|
if self._fn is None:
|
|
525
547
|
if t is MessageType.COMPLETE and len(self._deps) > 1:
|
|
@@ -623,7 +645,15 @@ class NodeImpl[T]:
|
|
|
623
645
|
) -> None:
|
|
624
646
|
if self._terminal and self._resubscribable:
|
|
625
647
|
self._terminal = False
|
|
648
|
+
lock = self._cache_lock
|
|
649
|
+
if lock is not None:
|
|
650
|
+
with lock:
|
|
651
|
+
self._cached = _SENTINEL
|
|
652
|
+
else:
|
|
653
|
+
self._cached = _SENTINEL
|
|
626
654
|
self._status = "disconnected" if self._has_deps else "settled"
|
|
655
|
+
if self._on_resubscribe is not None:
|
|
656
|
+
self._on_resubscribe()
|
|
627
657
|
|
|
628
658
|
h = hints or SubscribeHints()
|
|
629
659
|
self._sink_count += 1
|
|
@@ -726,11 +756,13 @@ class NodeImpl[T]:
|
|
|
726
756
|
lock = self._cache_lock
|
|
727
757
|
if lock is not None:
|
|
728
758
|
with lock:
|
|
729
|
-
|
|
730
|
-
|
|
759
|
+
v = self._cached
|
|
760
|
+
else:
|
|
761
|
+
v = self._cached
|
|
762
|
+
return None if v is _SENTINEL else v
|
|
731
763
|
|
|
732
764
|
@property
|
|
733
|
-
def last_mutation(self) ->
|
|
765
|
+
def last_mutation(self) -> MutationRecord | None:
|
|
734
766
|
"""Last non-internal ``write`` attribution (``actor``, ``timestamp_ns``), if any."""
|
|
735
767
|
return self._last_mutation
|
|
736
768
|
|
|
@@ -760,7 +792,7 @@ class NodeImpl[T]:
|
|
|
760
792
|
self._hash_fn = hash_fn
|
|
761
793
|
self._versioning = create_versioning(
|
|
762
794
|
level,
|
|
763
|
-
self._cached,
|
|
795
|
+
None if self._cached is _SENTINEL else self._cached,
|
|
764
796
|
id=id,
|
|
765
797
|
hash_fn=self._hash_fn,
|
|
766
798
|
)
|
|
@@ -960,4 +992,13 @@ def node(
|
|
|
960
992
|
# Public alias for type hints
|
|
961
993
|
Node = NodeImpl
|
|
962
994
|
|
|
963
|
-
__all__ = [
|
|
995
|
+
__all__ = [
|
|
996
|
+
"NO_VALUE",
|
|
997
|
+
"Node",
|
|
998
|
+
"NodeActions",
|
|
999
|
+
"NodeFn",
|
|
1000
|
+
"NodeImpl",
|
|
1001
|
+
"NodeStatus",
|
|
1002
|
+
"SubscribeHints",
|
|
1003
|
+
"node",
|
|
1004
|
+
]
|
|
@@ -306,7 +306,7 @@ def emit_with_batch(
|
|
|
306
306
|
messages: Messages,
|
|
307
307
|
*,
|
|
308
308
|
phase: int = 2,
|
|
309
|
-
strategy: EmitStrategy = "
|
|
309
|
+
strategy: EmitStrategy = "partition",
|
|
310
310
|
defer_when: DeferWhen = "batching",
|
|
311
311
|
subgraph_lock: object | None = None,
|
|
312
312
|
) -> None:
|
|
@@ -428,11 +428,11 @@ def _emit_sequential(
|
|
|
428
428
|
|
|
429
429
|
queue.append(_wrap_deferred_subgraph(_emit, subgraph_lock))
|
|
430
430
|
elif kind in _TERMINAL_TYPES and _should_defer_phase2(bs, defer_when):
|
|
431
|
-
# Terminal:
|
|
431
|
+
# Terminal: always route to phase-3 queue so all phase-2 drains first.
|
|
432
432
|
def _emit_term(m: Message = msg, s: Callable[[Messages], None] = sink) -> None:
|
|
433
433
|
s([m])
|
|
434
434
|
|
|
435
|
-
|
|
435
|
+
bs.pending_phase3.append(_wrap_deferred_subgraph(_emit_term, subgraph_lock))
|
|
436
436
|
else:
|
|
437
437
|
sink([msg])
|
|
438
438
|
|
|
@@ -14,7 +14,10 @@ def state(initial: Any, **opts: Any) -> Node[Any]:
|
|
|
14
14
|
"""Create a manually-settable source node with a fixed initial value.
|
|
15
15
|
|
|
16
16
|
Args:
|
|
17
|
-
initial: The initial cached value for the node.
|
|
17
|
+
initial: The initial cached value for the node. Because ``initial`` is
|
|
18
|
+
provided, ``equals`` is called on the first ``down()`` emission — if
|
|
19
|
+
the value matches ``initial``, the node emits ``RESOLVED`` instead
|
|
20
|
+
of ``DATA`` (spec §2.5).
|
|
18
21
|
**opts: Additional node options passed through to :func:`~graphrefly.core.node.node`.
|
|
19
22
|
|
|
20
23
|
Returns:
|