decent-newsroom/expression-bundle
decent-newsroom/expression-bundle is a reusable Symfony bundle for evaluating Nostr
feed expressions and spells.
It provides the expression parser, source resolvers, pipeline runner,
traversal operations, per-user result caching, and an optional authenticated
HTTP endpoint. Nostr transport is implemented with
innis/nostr-core and
innis/nostr-client.
30880 expression events.777 spells.GET /api/feed/{naddr} for authenticated evaluations.innis/nostr-core ^0.3.17.innis/nostr-client ^0.1.7.The package uses the DecentNewsroom\ExpressionBundle namespace. Its Composer
mapping is package-local and does not require the consuming application's
App\ classes.
Install the package from its Composer repository:
composer require decent-newsroom/expression-bundle
During local development, a Symfony host can consume the package through a path repository:
{
"repositories": [
{
"type": "path",
"url": "packages/expression-bundle",
"options": {
"symlink": true
}
}
],
"require": {
"decent-newsroom/expression-bundle": "@dev"
}
}
Register the bundle if Symfony Flex has not done so:
use DecentNewsroom\ExpressionBundle\ExpressionBundle;
return [
ExpressionBundle::class => ['all' => true],
];
Import its routes when the HTTP endpoint is needed:
# config/routes/expression.yaml
expression_bundle:
resource: '@ExpressionBundle/Resources/config/routes.yaml'
The bundle uses the expression configuration root:
# config/packages/expression.yaml
expression:
cache_ttl: 300
max_depth: 5
max_execution_time: 30
All values are optional. The defaults are 300 seconds for cached results, five levels of nested expression traversal, and 30 seconds of evaluation time.
The bundle is deliberately storage- and policy-agnostic. A consuming application must provide the relay contracts and may optionally provide a generic local event store:
| Contract | Responsibility |
|---|---|
EventInterface |
Scalar access to a Nostr event. |
EventStoreInterface |
Optional generic local event lookup, filtering, and reference queries. |
RelaySelectorInterface |
Default, content, author, and local-relay selection plus URL normalization. |
RelayEventClientInterface |
Relay event fetching. The package includes InnisRelayEventClient. |
UserRelayProviderInterface |
Optional per-user relay resolution for runtime context and relay probes. |
The host should alias each required interface to its adapter:
services:
DecentNewsroom\ExpressionBundle\Contract\EventStoreInterface:
alias: App\Integration\ExpressionEventStore
DecentNewsroom\ExpressionBundle\Contract\RelaySelectorInterface:
alias: App\Integration\ExpressionRelaySelector
DecentNewsroom\ExpressionBundle\Contract\RelayEventClientInterface:
alias: DecentNewsroom\ExpressionBundle\Infrastructure\InnisRelayEventClient
DecentNewsroom\ExpressionBundle\Contract\UserRelayProviderInterface:
alias: App\Integration\ExpressionUserRelayProvider
EventStoreInterface and UserRelayProviderInterface are optional. Without a
local event store, all event, list, filter, and traversal lookups use relays.
Without a user relay provider, evaluations still work using the host's default
relay selection.
InnisRelayEventClient uses the Innis client port
(Innis\Nostr\Client\Application\Port\NostrClientInterface). The recommended
way to supply it is decent-newsroom/nostr-client-bundle
(DecentNewsroom\NostrClientBundle), which wires the same
innis/nostr-client factory with configurable connection/reconnect defaults
(see ../nostr-client-bundle/docs/nostr-client-bundle.md in this packages directory). If that
bundle is not installed, the host can wire the port manually instead:
services:
Innis\Nostr\Client\Application\Port\NostrClientInterface:
factory: ['Innis\Nostr\Client\Infrastructure\Factory\NostrClientFactory', 'create']
arguments:
- '@logger'
Innis\Nostr\Core\Domain\Service\Bech32EncoderInterface:
class: Innis\Nostr\Core\Infrastructure\Adapter\Bech32EncoderAdapter
Relay communication is read-only in this bundle. The built-in client connects
to the relay URLs selected by RelaySelectorInterface, builds Innis filters,
subscribes, and converts received events to the package EventInterface
representation.
ExpressionService exposes cached and uncached evaluation methods:
evaluate() and evaluateSpell()evaluateCached() and evaluateSpellCached()getCachedResults() and getCachedSpellResults()The package owns cache-key generation and result caching. By default,
FeedCacheService uses Symfony's cache.app pool. Hosts can provide a
dedicated pool:
# config/packages/cache.yaml
framework:
cache:
pools:
expression.cache:
adapter: cache.adapter.redis
provider: Redis
default_lifetime: 300
# config/services.yaml
services:
DecentNewsroom\ExpressionBundle\Service\FeedCacheService:
arguments:
$cache: '@expression.cache'
$expressionCacheTtl: '%expression.cache_ttl%'
Cache keys include the expression coordinate and publication timestamp, the user pubkey, contacts, and interests. Republishing an expression therefore does not reuse the previous result cache.
The optional authenticated endpoint is:
GET /api/feed/{naddr}?offset=0&limit=50
It requires ROLE_USER, decodes the Nostr address, evaluates the expression
for the authenticated user, and returns:
{
"expression": "30880:pubkey:identifier",
"count": 1,
"offset": 0,
"limit": 50,
"events": []
}
The response limit is capped at 500 events.
ExpressionService creates a user runtime context.EventResolver queries the optional generic local store and relays.NormalizedItem[] result is returned or cached.The runtime context can include:
3 contacts;10015 interests;10002 user read relays.Install the package dependencies and run its package-owned test suite:
composer install
vendor/bin/phpunit -c phpunit.xml.dist
The package tests use ArrayEvent and contract mocks, so they do not require a
Doctrine entity or a newsroom database.
This package does not provide:
Those concerns belong to the consuming Symfony host and are connected through the package contracts and service configuration.
How can I help you explore Laravel packages today?