Skip to content

feat(spf): architecture reactors#1218

Merged
cjpillsbury merged 79 commits intomainfrom
feat/spf-architecture-reactors-texttracks
Apr 8, 2026
Merged

feat(spf): architecture reactors#1218
cjpillsbury merged 79 commits intomainfrom
feat/spf-architecture-reactors-texttracks

Conversation

@cjpillsbury
Copy link
Copy Markdown
Collaborator

@cjpillsbury cjpillsbury commented Apr 3, 2026

Resolves #1158

Summary

Uses SPF's text track implementation as the proving ground for the Actor/Reactor architecture described in internal/design/spf/primitives.md. The previous text track code bypassed the Task/TaskRunner/Actor/Reactor primitives — this branch rebuilds it from the ground up, producing reference implementations that inform the eventual media (video/audio) revisit.

What changed

New primitives and factories:

  • createMachineActor — declarative FSM-based actor factory with per-state message handlers, optional runner, and reactive snapshot
  • createTransitionActor — reducer-style actor for reactive context without FSM states
  • createMachineReactor — signal-driven reactor factory with monitor, entry/effects split, and reactive snapshot
  • createMachineCore — shared FSM kernel (snapshot signal, transition, state reader) consumed by both factories
  • Machine type hierarchy (MachineSnapshot, ActorSnapshot, Machine, SignalActor)
  • SerialRunner.whenSettled() / SerialRunner.abortPending() for coordinated task lifecycle

Text track Actor/Reactor implementations:

  • TextTracksActor — manages the native TextTrackList (modes, cues, loaded/segments context) via createTransitionActor
  • TextTrackSegmentLoaderActor — plans and executes VTT segment fetches via CallbackActor interface
  • syncTextTracks — FSM reactor consolidating the three previous text track sync functions into a single reactor with bidirectional sync
  • loadTextTrackCues — reactor coordinating segment loading via Actor messages

Migrations to new factories:

  • SourceBufferActorcreateMachineActor
  • SegmentLoaderActorcreateMachineActor
  • resolvePresentationcreateMachineReactor (4-state FSM)
  • trackPlaybackInitiatedcreateMachineReactor

State cleanup:

  • Removed textBufferState from PlaybackEngineState (now owned by TextTrackSegmentLoaderActor)
  • Removed owners.textTracks map in favor of mediaElement.textTracks lookup
  • Deleted superseded reactors: setupTextTracks, syncTextTrackModes, syncSelectedTextTrackFromDom

Design docs:

  • actor-reactor-factories.md — decided factory API, XState comparison, open questions
  • signals.md — decision rationale, tradeoffs, friction points
  • text-track-architecture.md — reference implementation assessment
  • Updated primitives.md and index.md (glossary, terminology disambiguation)
  • Disambiguated "observable" terminology: "reactive" for signals-based snapshots/state, "Observable" reserved for RxJS/TC39 pattern

Bug fixes:

  • Resolved pre-existing test failures (fetch pollution, paused mock)
  • Added destroyed guard to TextTrackSegmentLoaderActor
  • Fixed typecheck errors (machine.ts generic constraint, exactOptionalPropertyTypes in test)

Test plan

  • pnpm -F @videojs/spf test — all SPF tests pass
  • pnpm typecheck — clean across repo
  • pnpm lint — no new violations
  • Manual verification in sandbox: load HLS stream with subtitles, toggle tracks, verify cue rendering and mode sync
  • Verify engine destroy() tears down all actors and aborts in-flight work

🤖 Generated with Claude Code


Note

Medium Risk
Introduces new core state-machine factories and changes task runner scheduling/settlement semantics, which can subtly affect async ordering and teardown across playback features.

Overview
Adds declarative state-machine primitives to SPF: createMachineActor, createTransitionActor, and createMachineReactor, backed by a shared createMachineCore/Machine snapshot type, and updates SignalActor/CallbackActor typing around snapshot.value + context.

Extends SerialRunner/ConcurrentRunner with whenSettled() and improves cancellation controls (SerialRunner.abortPending()), enabling actors to auto-return via per-state onSettled and to differentiate continue vs preempt behaviors.

Migrates resolvePresentation from ad-hoc effects to a createMachineReactor FSM with abortable fetch on state exit, updates dependent tests, and aligns call sites like end-of-stream to the new snapshot.value field. Also includes substantial SPF design documentation additions/updates describing the factories, signals decision, and migration notes.

Reviewed by Cursor Bugbot for commit c2d19b4. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Apr 8, 2026 7:18pm

Request Review

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 3, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit c2d19b4
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69d6aa00df739a0007c7a041
😎 Deploy Preview https://deploy-preview-1218--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@cjpillsbury cjpillsbury changed the title Feat/spf architecture reactors texttracks feat(spf): architecture reactors Apr 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

📦 Bundle Size Report

🎨 @videojs/html

Path Base PR Diff %
/media/simple-hls-video 15.08 kB 15.87 kB +812 B +5.3% 🔺
Presets (7)
Entry Size
/video (default) 26.55 kB
/video (default + hls) 158.81 kB
/video (minimal) 24.95 kB
/video (minimal + hls) 157.28 kB
/audio (default) 24.61 kB
/audio (minimal) 23.11 kB
/background 4.16 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.73 kB
/media/dash-video 236.51 kB
/media/hls-video 133.61 kB
/media/mux-audio 156.33 kB
/media/mux-video 156.22 kB
/media/native-hls-video 3.62 kB
/media/simple-hls-video 15.87 kB
Players (3)
Entry Size
/video/player 6.34 kB
/audio/player 4.85 kB
/background/player 3.86 kB
Skins (17)
Entry Type Size
/video/minimal-skin.css css 3.43 kB
/video/skin.css css 3.45 kB
/video/minimal-skin js 24.94 kB
/video/minimal-skin.tailwind js 25.21 kB
/video/skin js 26.57 kB
/video/skin.tailwind js 26.74 kB
/audio/minimal-skin.css css 2.49 kB
/audio/skin.css css 2.46 kB
/audio/minimal-skin js 23.09 kB
/audio/minimal-skin.tailwind js 23.27 kB
/audio/skin js 24.62 kB
/audio/skin.tailwind js 24.81 kB
/background/skin.css css 117 B
/background/skin js 1.15 kB
/base.css css 157 B
/shared.css css 88 B
/skin-element js 1.35 kB
UI Components (23)
Entry Size
/ui/alert-dialog 994 B
/ui/alert-dialog-close 464 B
/ui/alert-dialog-description 393 B
/ui/alert-dialog-title 401 B
/ui/buffering-indicator 2.03 kB
/ui/captions-button 2.07 kB
/ui/controls 1.75 kB
/ui/error-dialog 2.55 kB
/ui/fullscreen-button 2.07 kB
/ui/hotkey 2.21 kB
/ui/mute-button 2.05 kB
/ui/pip-button 2.05 kB
/ui/play-button 2.01 kB
/ui/playback-rate-button 2.04 kB
/ui/popover 1.76 kB
/ui/poster 1.76 kB
/ui/seek-button 2.04 kB
/ui/slider 1.56 kB
/ui/thumbnail 2.52 kB
/ui/time 2.03 kB
/ui/time-slider 3.33 kB
/ui/tooltip 1.97 kB
/ui/volume-slider 2.13 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react

Path Base PR Diff %
/media/simple-hls-video 15.18 kB 15.92 kB +757 B +4.9% 🔺
Presets (7)
Entry Size
/video (default) 21.40 kB
/video (default + hls) 153.81 kB
/video (minimal) 19.94 kB
/video (minimal + hls) 152.40 kB
/audio (default) 18.18 kB
/audio (minimal) 16.74 kB
/background 754 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 236.42 kB
/media/hls-video 133.55 kB
/media/mux-audio 156.34 kB
/media/mux-video 156.26 kB
/media/native-hls-video 3.65 kB
/media/simple-hls-video 15.92 kB
Skins (14)
Entry Type Size
/video/minimal-skin.css css 3.36 kB
/video/skin.css css 3.38 kB
/video/minimal-skin js 19.88 kB
/video/minimal-skin.tailwind js 23.32 kB
/video/skin js 21.29 kB
/video/skin.tailwind js 23.38 kB
/audio/minimal-skin.css css 2.38 kB
/audio/skin.css css 2.34 kB
/audio/minimal-skin js 16.65 kB
/audio/minimal-skin.tailwind js 19.13 kB
/audio/skin js 18.09 kB
/audio/skin.tailwind js 19.13 kB
/background/skin.css css 90 B
/background/skin js 272 B
UI Components (19)
Entry Size
/ui/alert-dialog 1.11 kB
/ui/buffering-indicator 1.39 kB
/ui/captions-button 2.18 kB
/ui/controls 1.87 kB
/ui/error-dialog 1.97 kB
/ui/fullscreen-button 2.14 kB
/ui/mute-button 2.16 kB
/ui/pip-button 2.17 kB
/ui/play-button 2.17 kB
/ui/playback-rate-button 2.17 kB
/ui/popover 1.86 kB
/ui/poster 1.70 kB
/ui/seek-button 2.16 kB
/ui/slider 3.05 kB
/ui/thumbnail 2.13 kB
/ui/time 1.72 kB
/ui/time-slider 2.76 kB
/ui/tooltip 2.22 kB
/ui/volume-slider 2.73 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core

Path Base PR Diff %
/dom/media/simple-hls 14.45 kB 15.31 kB +878 B +5.9% 🔺
Entries (8)
Entry Size
. 5.29 kB
/dom 10.49 kB
/dom/media/custom-media-element 1.91 kB
/dom/media/dash 235.94 kB
/dom/media/hls 132.95 kB
/dom/media/mux 155.72 kB
/dom/media/native-hls 2.97 kB
/dom/media/simple-hls 15.31 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 999 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.38 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.74 kB
/events 319 B
/function 327 B
/object 247 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B

📦 @videojs/spf

Path Base PR Diff %
/dom 12.45 kB 13.27 kB +841 B +6.6% 🔺
/playback-engine 12.41 kB 13.10 kB +705 B +5.5% 🔺
Entries (3)
Entry Size
. 40 B
/dom 13.27 kB
/playback-engine 13.10 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

cjpillsbury and others added 20 commits April 8, 2026 10:44
Replaces setupTextTracks, syncTextTrackModes, and syncSelectedTextTrackFromDom
with a single syncTextTracks function implementing a 5-state FSM
(preconditions-unmet → setting-up → set-up → destroying → destroyed).
Each FSM state is managed by a dedicated effect; entry/exit actions map to
the effect body and its returned cleanup function respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empirical testing confirmed cues added via addCue() survive a
disabled → showing mode transition when no src is set on the <track>
element. The textBufferState clearing on deselect in the old
syncSelectedTextTrackFromDom was therefore unnecessary.

Removes the textBufferState field and TextTrackBufferState import from
sync-text-tracks.ts, and adds a browser test documenting the verified
cue preservation behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setupTextTracks, syncTextTrackModes, and syncSelectedTextTrackFromDom are
fully replaced by syncTextTracks. Removes the six source and test files,
and drops the now-incorrect src assertion from the playback-engine test
(track elements no longer have src set).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
….textTracks lookup

loadTextTrackCues no longer requires a pre-built Map<id, HTMLTrackElement> in
owners. It now holds a mediaElement reference and looks up the native TextTrack
by id via Array.from(mediaElement.textTracks).find(t => t.id === trackId).

syncTextTracks no longer builds or writes the map. PlaybackEngineOwners no
longer carries the textTracks field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…/re-add

change events on TextTrackList are queued as tasks (async per spec), so
syncModes inside onChange cannot cause re-entrant calls. The syncTimeout
guard is sufficient; removing and re-adding the listener on each settling-
window change event was unnecessary complexity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wns cue operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ve textBufferState from state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… to Actor/Reactor pattern

Implements TextTrackSegmentLoaderActor as a proper SignalActor using
Task/SerialRunner primitives, and simplifies loadTextTrackCues to a thin
Reactor that delegates all async work to the Actor layer.

Key decisions:
- SerialRunner chosen over ConcurrentRunner — VTT segments are small,
  serial execution keeps the generation-counter pattern simple
- Empty object context — all loaded-segment bookkeeping lives in
  TextTracksActor, not the loader
- untrack() wraps textTracksActor.snapshot.get() inside send() to prevent
  the Reactor's effect from subscribing to the TextTracksActor snapshot,
  which would cause spurious re-triggers on every cue add

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rceBufferActor

Introduces CueSegmentMeta (id + startTime + duration + trackId) on
AddCuesMessage, mirroring AppendSegmentMeta on AppendSegmentMessage.
TextTracksActorContext.segments now stores full segment timing instead of
bare { id } records.

This lets TextTrackSegmentLoaderActor pass context.segments[trackId]
directly to getSegmentsToLoad, eliminating the id→object reconstruction
and the redundant id-based double-filter that worked around the mismatch.

Also removes the distinct 'done' status (collapses back to 'idle') and
the redundant #destroyed flag (status signal is the source of truth).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…derActor

Exposes the runner's current chain tail as a read-only `settled` getter.
Capturing it after scheduling a batch and comparing identity in the
resolution callback replaces the #loadGeneration counter, Promise.all
collection, and .catch() in TextTrackSegmentLoaderActor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Narrows bufferedSegments to Pick<Segment, 'startTime' | 'duration'> —
the only fields the function actually uses. Removes the assumption that
callers must hold full Segment objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces `createActor` — a declarative factory for building message-driven
actors with finite state, per-state message handlers, an optional task runner,
and automatic `onSettled` transitions.

`SerialRunner.whenSettled(callback)` owns the generation-token logic internally,
eliminating the need for a runner wrapper or scheduling-tracking flag in the actor.
The raw runner is passed directly to handlers.

Design documented in `internal/design/spf/actor-reactor-factories.md`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tory

Replaces the class with a `createTextTrackSegmentLoaderActor` factory function
using the new `createActor` primitive. The `runner.settled` promise + manual
generation-token pattern is replaced by `onSettled: 'idle'` at the state level,
delegating all generation-token logic to `SerialRunner.whenSettled`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the TextTracksActor class with a createTextTracksActor factory
function using the createActor primitive. Context mutations via direct
signal updates become setContext() calls; mediaElement is captured in
the factory closure rather than held as a private field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…formance for ConcurrentRunner

- Merge SettledRunnerLike into RunnerLike — whenSettled is now required on all runners
- Remove hasWhenSettled type guard; simplify onSettled check in createActor
- Add whenSettled, destroy, and settled-promise tracking to ConcurrentRunner
- Change ConcurrentRunner.schedule() to return Promise<TValue>; return in-flight promise on dedupe
- Add ConcurrentRunner tests: whenSettled, destroy, deduplication returns same promise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces createReactor — the declarative counterpart to createActor for
signal-driven reactive logic. Each state holds an array of effect functions
(one independent effect() per element) with its own dependency tracking and
cleanup lifecycle. Snapshot shape matches actors: { status, context }.
'destroying' and 'destroyed' are implicit terminal states; destroy() transitions
through both synchronously, leaving room for async teardown in the future.

Migrates syncTextTracks to createReactor, replacing four named effect closures
with a declarative state definition. Moves selectedTextTrackId reset from the
mode-sync effect to the state-guard exit cleanup (correct cohesion). Return type
changes from () => void to Reactor for future status observability; engine
cleanups forEach updated to dispatch by shape.

Documents segment-actor migration assessment in .claude/plans/spf/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tatus

States are now preconditions-unmet → idle → resolving → resolved, with
mutually-exclusive conditions derived by deriveStatus(). Each state has a
single condition monitor that reads derivedStatusSignal and transitions
directly to whatever state conditions currently dictate — including
mid-resolve condition changes. resolving additionally carries a fetch task
that returns its AbortController so createReactor aborts it on exit.

createReactor now supports { abort(): void } as an effect return type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After update(state, { presentation: parsed }), derivedStatusSignal
becomes 'resolved' automatically, which triggers the resolving condition
monitor to transition. No need for the fetch task to call transition
directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cjpillsbury and others added 18 commits April 8, 2026 10:44
Replace the manual signal() + update() plumbing with createMachineCore,
consistent with createActor and createReactor. getState() replaces
inline snapshotSignal.get().value reads; transition() replaces
update(snapshotSignal, { value }). No behaviour change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add BatchMessage = { type: 'batch'; messages: IndividualSourceBufferMessage[] }
to the SourceBufferMessage union and handle it in send() alongside the
existing individual message types. Remove the separate batch() method
from the interface and implementation — send() is now the single entry
point for all SourceBuffer operations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…via waitForIdle

SourceBufferActor.send() now returns void. SegmentLoaderActor sequences
SourceBuffer operations by watching snapshot state transitions via a
waitForIdle() helper (effect-based promise) instead of awaiting the
send() return value. Removes SourceBufferActorError — errors are logged
and the actor self-heals to idle. Tests updated to use vi.waitFor() for
actor settling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…into message

SourceBufferActor is now built with createActor<'idle'|'updating', ...>. The
'updating' state uses onSettled:'idle' to auto-transition after tasks settle.
Each SourceBufferMessage carries its own AbortSignal as a message field rather
than a separate send() argument, making the public API send(message):void and
consistent with the rest of the actor taxonomy.

Call sites in SegmentLoaderActor and tests updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l comparison

Documents the SPF vs XState behavioral divergence around where async work
"belongs" (idle handler vs updating-on-entry invoke), consequences for
atomicity/lifecycle/partial-updates, tradeoffs of adopting the XState model,
and the state-scoped runner as a middle-ground open question.

Updates the runner lifetime section to reflect that state-scoped runners are
a revisit candidate rather than a firm rejection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in batch

Adds getContext() — a live untracked read — to HandlerContext alongside the
existing context snapshot. Tasks scheduled on the runner use getCtx: getContext
so each task reads the context committed by the previous one, rather than the
stale dispatch-time snapshot.

This eliminates the workingCtx threading pattern in SourceBufferActor's batch
handler: every task now calls setContext on completion, publishing its result
immediately. Context accurately reflects what has actually been appended as it
happens, consistent with the existing onPartialContext model for streaming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Align task factory parameter names with HandlerContext.getContext for
consistency across the actor API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns MessageTaskOptions with HandlerContext naming — both now use
setContext — removing the partial-specific name that implied a narrower
contract than the function actually has.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the Open Questions entry with a documented decision: actor-lifetime
runners stay for now. Explains why state-scoped runners require more than
convention (onEnter + context side channel), and the condition under which
we'd revisit. Also removes the Option C code example from the XState
middle-ground section and cleans up stale getCtx/onPartialContext references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cancellation is now an explicit actor message rather than an out-of-band
signal threaded through every send(). SourceBufferActor gains a `cancel`
handler in `updating` that calls runner.abortAll(); SegmentLoaderActor
sends cancel only when preempting with a track switch or segment abort,
preserving the same-track-seek init-commit optimisation without the
appendSignal hack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the three identical append-init/append-segment/remove handler
bodies with a shared onMessage closure, using HandlerContext to type
the parameters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Runner.abortPending()

- Add `abortPending()` to SerialRunner: aborts only queued tasks without
  touching the in-flight task, enabling the "continue" case in load handlers
- Migrate createSegmentLoaderActor from CallbackActor closure to createActor
  with idle/loading states and SegmentLoaderActorContext tracking in-flight
  init and segment IDs
- Replace executeLoadTask/runScheduled/abortController closure state with
  makeLoadTask factory + SerialRunner scheduling
- loading.on.load: abortPending() for continue (in-flight still needed),
  abortAll() + optional cancel message for preempt (track switch / seek away)
- On unexpected fetch errors, abort pending tasks so a failed init does not
  allow segment fetches to proceed with no init segment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r migration

Replace stale "~50 lines of abort signal management" claim with the actual
mechanism: explicit runner.abortAll/abortPending in handlers + cancel message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents message dispatch and double-destroy after the actor is torn
down, matching the destroy pattern used by createMachineActor and
createTransitionActor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r/createMachineReactor

Makes the FSM nature of these factories explicit, distinguishing them
from createTransitionActor (reducer-style, no state machine) and manual
CallbackActor implementations.

Renames files, exports, all consumers, tests, and design doc references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace stale actor examples in actor-reactor-factories.md with
  SourceBufferActor and SegmentLoaderActor (the actual consumers)
- Add actor/reactor type taxonomy table documenting all four patterns
- Fix HandlerContext shape (was missing getContext)
- Update primitives.md: SourceBufferActor migrated, factory not class,
  monitor/entry/reactions terminology, entry/reactions now decided
- Update text-track-architecture.md: fix actor descriptions, code
  examples to use monitor/entry/reactions, mark implemented futures
- Update decisions.md and signals.md: always → monitor terminology
- Update actor-migration-assessment.md: mark completed migrations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `effects` name directly maps to the underlying signal effect()
primitive, making it immediately clear how tracked re-runs work.
The `entry` / `effects` pairing is natural: entry is the special
untracked one-time thing, effects are normal reactive signal effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use "reactive" for signals-based snapshots/context/state (the SPF
primitive) and reserve "observable"/"Observable" for the RxJS/TC39
Observable pattern discussed as an alternative. Adds glossary and
structure note to spf/index.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nitiated test

Use function form of `update()` in `createMachineCore` transition to
satisfy generic `Partial<Snapshot>` constraint. Omit `url` property
instead of setting it to `undefined` to satisfy `exactOptionalPropertyTypes`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cjpillsbury cjpillsbury marked this pull request as ready for review April 8, 2026 20:37
@cjpillsbury cjpillsbury merged commit 1346d86 into main Apr 8, 2026
23 checks passed
@cjpillsbury cjpillsbury deleted the feat/spf-architecture-reactors-texttracks branch April 8, 2026 20:38
@luwes luwes mentioned this pull request Apr 8, 2026
@github-actions github-actions bot mentioned this pull request Apr 8, 2026
1 task
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c2d19b4. Configure here.

.catch((error) => {
if (error instanceof Error && error.name === 'AbortError') return;
throw error;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reactor stuck in 'resolving' after fetch error

Medium Severity

The resolving entry effect has no recovery path after a non-abort fetch error. The old effect-based code recovered because .finally() reset a resolving flag, allowing the effect to re-fire on the next state change (since state was tracked inside the effect body). The new reactor stays permanently stuck in 'resolving' because deriveState returns 'resolving' for any unresolved URL with shouldResolve met, and entry effects only run on state entry — not re-entry to the same state. A subsequent URL change (replacing one unresolved presentation with another) produces the same derived state, so no transition fires and no new fetch starts.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c2d19b4. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Architecture: Text Track Spike — Actors, Reactors, and Primitives Reference Implementations

1 participant