Skip to content

feat: bring the plugin up to the current framework - #2

Merged
mikield merged 2 commits into
mainfrom
feat/port-to-framework-0.10
Sep 2, 2026
Merged

mikield merged 2 commits into
mainfrom
feat/port-to-framework-0.10

Conversation

@mikield

@mikield mikield commented Sep 2, 2026

Copy link
Copy Markdown
Member

The plugin was written against framework 0.6 and does not load against 0.10. Everything it reached for on the framework side has since moved:

Referenced Now
Tempcord\Contract\CanBeHandled gone
Tempcord\Support\Commands\CommandHandler gone
Tempcord\Plugins\IsPlugin gone
Plugin::boot() takes the Tempcord instance
Ragnarok\Fenrir\Discord Tempcord\Discord\Discord
Tempest\get Tempest\Container\get

composer.json also pinned tempest/* ^2 against a framework on ^3.18, and carried a hardcoded "version": "0.5.0" — the same thing we took out of the other repos so Packagist reads tags instead.

#[Task] now goes on a class or a method

Per the discussion: on an invokable class it reads like every other Tempcord attribute, where a task is one class; on a method it still groups several chores that genuinely belong together.

#[Task(interval: 10)]
final readonly class SweepTemporaryMessages
{
    public function __invoke(): void { /* ... */ }
}

Attributes stopped carrying state

Tasks compile to a TaskDefinition instead of the attribute being handed a reflector via setReflector() and passed around. An attribute that mutates cannot survive the discovery cache — which is what the old createCachePayload() was working around, by serializing the whole Registry and then foreach-ing the unserialized object as if it were a list. That path was a fatal the first time it ran.

Each turn runs in a fiber

invoke() was called synchronously, so no task could await the REST API — for a Discord bot that is most of what a scheduled task wants to do. Turns now run the way command handlers do. Alongside that: a turn is skipped while the previous one is still running, and a task that throws is logged and keeps its place rather than cancelling its own timer and silently never running again.

Two cron bugs

  • A step over a range counted from the bottom of the field. 1-10/2 gave 2,4,6,8,10 instead of 1,3,5,7,9. The README documents 1-10/2 as a supported form.
  • Both day fields restricted meant "and" where cron means "or". 0 0 1 * 1 ran only on Mondays that happened to fall on the first — roughly once a year instead of thirteen times.

Cron tasks are also armed for the exact wait until the next matching minute and re-armed from the turn just taken, rather than waking every 60s to ask whether it is time. The old arming drifts from whatever offset the bot started at, and once the drift crosses a boundary a matching minute is stepped over and the task silently does not run that hour.

Smaller things

  • An ordinary turn logs at debug. A task running every ten seconds was writing ~8,600 info lines a day to say nothing happened.
  • Task::$handler was a property hook returning $this->handler — reading it once would have overflowed the stack.
  • Registry::getStats() returned the Runner where an array was declared.
  • Default names are now ClassName / ClassName::method. A bare method name collides between classes, and two tasks sharing a name share their statistics and cannot be cancelled apart.

Tests

53, where there were none, driving a fake loop so the suite does not wait out the schedules it exercises. phpstan at level 5, matching the framework.

Not done here, and worth a separate decision: this repo has no CI and no release workflow, unlike the other three.

🤖 Generated with Claude Code

Rebased onto #1, which moved the plugin to Tempest 3 and the current
Plugin contract. What remained was the parts that still could not work.

ragnarok/fenrir was still required and Registry still implemented its
Extension, so registerExtension() was handed something the current
library's signature does not accept. The plugin no longer registers an
extension at all: registerExtension() calls initialize() immediately, so
routing through it bought indirection and no ordering. The registry is
started from the plugin's own boot instead.

composer.json pinned tempcord/framework ^0.7, which under Composer's
reading of a 0.x caret excludes the 0.10 the plugin is meant to extend,
and carried a hardcoded version, which is the thing we took out of the
other repos so Packagist reads tags.

#[Task] now goes on an invokable class as well as a method, so a task
that is one class reads like every other Tempcord attribute.

Tasks compile to a TaskDefinition rather than the attribute being handed
a reflector and passed around. An attribute that mutates cannot survive
the discovery cache.

Each turn runs in a fiber, so a task may await the REST API — which for a
Discord bot is most of what a task wants to do. A turn is skipped while
the previous one is still running, and a task that throws is logged and
keeps its place instead of cancelling its own timer.

Two cron bugs: a step over a range counted from the bottom of the field,
so 1-10/2 gave 2,4,6,8,10 rather than 1,3,5,7,9; and restricting both day
fields meant "and" where cron means "or", so 0 0 1 * 1 ran only on
Mondays that fell on the first. Cron tasks are armed for the exact wait
until the next matching minute rather than waking every minute, which
drifts until a matching minute is stepped over entirely.

An ordinary turn logs at debug: a task running every ten seconds was
writing eight thousand info lines a day to say nothing happened.

67 tests driving a fake loop, phpstan at the level the framework uses,
and the CI, release and contribution setup the other three repos have.
@mikield
mikield force-pushed the feat/port-to-framework-0.10 branch from 78fca60 to 3f54b8a Compare September 2, 2026 01:56
@mikield

mikield commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main — the conflicts came from #1, which had already moved the plugin to Tempest 3 and the current Plugin contract in parallel. I kept that work and its test coverage (discovery, registry dedup, the plugin contract, all adapted to the definitions API) and layered on only what #1 did not reach:

  • ragnarok/fenrir was still required, and Registry still implemented its Extension, so registerExtension() was being handed something the current library's signature does not accept. The plugin no longer registers an extension at all — registerExtension() calls initialize() immediately, so routing through it was indirection with no ordering benefit.
  • composer.json pinned tempcord/framework: ^0.7, which under Composer's reading of a 0.x caret is >=0.7 <0.8 — it excludes the 0.10 this is meant to extend. Now >=0.10 <1.0. The hardcoded "version": "0.7.0" is gone too.
  • #[Task] on a class as well as a method.
  • Fibers, the overlap skip, containment, the two cron bugs and the cron drift — as described above.

Also added the CI, release and contribution setup the other three repos have: tests, static analysis, PR-title lint, semantic-release on main, dependabot, CONTRIBUTING.

One thing needs your call before merging: this repo has no tags. semantic-release with no tag to start from will cut v1.0.0 for a feat. If the plugin should stay on its 0.x line, tag v0.7.0 on main first (matching the version composer.json used to declare) and the next release becomes v0.8.0. Say the word and I'll push the tag.

@mikield
mikield merged commit ef94e9a into main Sep 2, 2026
2 of 3 checks passed
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant