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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
push:
branches: ["**"]
pull_request:

jobs:
test:
name: Test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up BEAM
uses: erlef/setup-beam@v1
with:
otp-version: "26.2.5.8"
elixir-version: "1.18.3"

- name: Install dependencies
run: mix deps.get

- name: Check formatting
run: mix format --check-formatted

- name: Compile
run: mix compile --warnings-as-errors

- name: Audit dependencies
run: mix deps.audit

- name: Run Credo
run: mix credo --strict

- name: Run tests with coverage
run: mix test --cover

- name: Run Dialyzer
run: mix dialyzer --format short
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
erlang 26.2.5.8
elixir 1.17.3-otp-26
elixir 1.18.3-otp-26
53 changes: 53 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# AGENTS.md

Guidance for automated coding agents working in this repository.

## Project Context

Rocket is an Elixir library for Firebase Cloud Messaging HTTP v1. Phases 1-3 of the cleanup plan are complete: tests, reproducible tooling, correctness fixes, modernization, CI, static analysis, and documentation are in place. New features are still Phase 4 future work and should not be mixed into cleanup changes.

## Current State

- The supported local toolchain is Elixir 1.18.3 / Erlang 26.2.5.8.
- The test suite covers configuration, request handling, response parsing, default response logging, GenStage flow, supervision, and the public `Rocket.push/1` boundary.
- Built-in Mix coverage enforces a 90% threshold; the latest local run reported 91.43% total coverage.
- CI runs formatting, compilation with warnings as errors, dependency audit, Credo, tests with coverage, and Dialyzer.
- Response handlers implement `call/3`.
- HTTP clients implement `Rocket.HTTPClient`; Rocket defaults to `Rocket.HTTPClient.Finch`.
- `Rocket.push/1` is documented as the synchronous public request API.
- GenStage producer/consumer modules remain available for queued internal processing.
- Supervisor child specs and config syntax have been modernized.

## Working Rules

- Keep tests ahead of runtime behavior changes. Add or update tests that describe the intended behavior before changing it.
- Maintain very high coverage. Coverage should include error paths, not only happy paths.
- Avoid live network calls in tests. Stub or inject HTTP and Goth interactions.
- Preserve the existing public API unless a breaking change is deliberately documented and agreed.
- Keep cleanup PRs small and focused. Separate test scaffolding, toolchain updates, refactors, dependency changes, and future feature work.
- Do not commit secrets, Firebase credentials, service-account JSON, `.env` files, `_build/`, `deps/`, `cover/`, or generated docs.
- Prefer clear return values over log-only behavior when making correctness fixes, but document any API change before implementing it.

## Expected Commands

These commands should work from a fresh checkout:

```sh
mix deps.get
mix format --check-formatted
mix compile --warnings-as-errors
mix deps.audit
mix credo --strict
mix test
mix test --cover
mix dialyzer --format short
mix docs
```

If the selected coverage or static-analysis tools change, update this file and `TODO.md`.

## Suggested Cleanup Order

1. Keep Phase 1-3 maintenance checks green.
2. Treat Phase 4 new features as separate work.
3. For future features, add tests, docs, and compatibility notes with the implementation.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

## Unreleased

- Replaced HTTPoison with Finch for FCM HTTP requests.
- Added a supervised Finch pool named `Rocket.Finch`.
- Added `Rocket.HTTPClient` so applications can provide a custom HTTP client; Finch remains the default adapter and is declared as an optional direct dependency.
- Preserved the stable public API while preparing a minor `0.1.0` release.
- Added a high-coverage ExUnit test suite around request handling, response parsing, configuration, GenStage flow, and supervision.
- Updated the supported local toolchain to Elixir 1.18.3 / Erlang 26.2.5.8.
- Switched to built-in Mix coverage with a 90% threshold.
- Added CI for formatting, compilation, dependency audit, Credo, tests with coverage, and Dialyzer.
- Modernized supervisor child specs and configuration syntax.
- Fixed response handler callback compatibility.
- Normalized request and response errors into structured `{:ok, term}` and `{:error, term}` results.
- Added project documentation and package documentation metadata.
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Rocket

Rocket is an Elixir client for Firebase Cloud Messaging HTTP v1.

The current maintenance focus is test coverage, correctness, and modernization. New features are intentionally deferred until the cleanup baseline is stable.

## Requirements

- Elixir 1.18.x
- Erlang/OTP 26.2.x

The local toolchain is documented in `.tool-versions`.

## Installation

Add Rocket to your dependencies:

```elixir
def deps do
[
{:rocket, "~> 0.1"}
]
end
```

## Configuration

Rocket expects Google service-account credentials as JSON. The default configuration reads credentials from the `GCP_CREDENTIALS` environment variable:

```sh
export GCP_CREDENTIALS='{"project_id":"my-project", "...":"..."}'
```

The default response handler logs request outcomes:

```elixir
config :rocket,
response_handler: Rocket.Response.DefaultHandler,
workers: 2
```

Custom response handlers implement `Rocket.Response.ResponseHandler`.

## Usage

`Rocket.push/1` performs a synchronous FCM request and returns a structured result:

```elixir
payload = %{
"message" => %{
"token" => "device-token",
"notification" => %{
"title" => "Hello",
"body" => "World"
}
}
}

Rocket.push(payload)
```

Successful responses return `{:ok, decoded_body}`. Request, configuration, encoding, HTTP, and FCM errors return `{:error, reason}`.

The GenStage producer/consumer modules are used for queued internal processing. They are not the public boundary for new features unless that API is deliberately designed and documented.

## Development

Fetch dependencies:

```sh
mix deps.get
```

Run the verification suite:

```sh
mix format --check-formatted
mix compile --warnings-as-errors
mix deps.audit
mix credo --strict
mix test --cover
mix dialyzer --format short
```

Rocket defaults to Finch for HTTP requests and starts a supervised pool named `Rocket.Finch` when the default client is used. Finch is an optional dependency; applications that provide their own `:http_client` do not need to include it.

The coverage threshold is enforced by `mix test --cover` and is currently set to 90%.

## Release Notes

See `CHANGELOG.md`.
102 changes: 102 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# TODO

This project is an outdated Elixir library for sending Firebase Cloud Messaging HTTP v1 requests. The first phase should improve confidence, maintainability, and release hygiene without adding new user-facing features. New features belong in future work after the baseline is tested and modernized.

## Phase 1: Safety Baseline

- [x] Add a comprehensive test suite before changing runtime behavior.
- Target very high coverage, ideally 90%+ line coverage and meaningful branch/error coverage.
- Cover `Rocket.Config`, `Rocket.Request`, `Rocket.Response`, `Rocket.Response.DefaultHandler`, `Rocket.PushCollector`, `Rocket.Pusher`, and `Rocket.Application`.
- Include success responses, client errors, server errors, invalid JSON bodies, Finch errors, missing/invalid Goth configuration, and GenStage producer/consumer flow.
- Avoid live FCM/GCP calls in normal tests. Use explicit test doubles, dependency injection, or controlled mocks for Goth and HTTP clients.
- [x] Make the local toolchain reproducible.
- Resolved the mismatch between `mix.exs` and `.tool-versions`.
- Supported local toolchain is Elixir 1.18.3 / Erlang 26.2.5.8.
- Ensure `mix test`, `mix format --check-formatted`, and coverage commands run from a fresh checkout.
- [x] Add CI that runs formatting, compilation with warnings treated seriously, tests, and coverage reporting.
- [x] Decide whether to keep ExCoveralls or move to a simpler built-in coverage flow, then enforce a minimum coverage threshold.
- Moved to built-in Mix coverage with a 90% threshold.
- [x] Add or restore basic project documentation, including installation, configuration, usage, testing, and release notes.

## Phase 2: Correctness And Compatibility

- [x] Fix the `Rocket.Response.ResponseHandler` behaviour mismatch.
- The behaviour defines `call/3`, while `Rocket.Response.DefaultHandler` currently implements `call/2`.
- `Rocket.Request` currently calls `handler.call(status, payload, decoded_body)`, which will fail with the default handler.
- [x] Decide and document the public API boundary.
- Clarify whether `Rocket.push/1` is meant to perform a synchronous request or enqueue work through `Rocket.PushCollector`.
- Preserve backwards compatibility unless a breaking change is explicitly planned.
- [x] Normalize response handling.
- Avoid duplicated response parsing between `Rocket.Request` and `Rocket.Response`.
- Return structured success/error tuples where practical instead of logging-only outcomes.
- Keep logging useful but avoid making logs the only observable result.
- [x] Harden request error handling.
- Handle configuration failures without pattern-match crashes.
- Handle Finch transport errors beyond simple timeout cases.
- Avoid raising on JSON encoding failures unless that is intentionally part of the API.
- [x] Add tests for concurrency and supervision.
- Verify the configured worker count.
- Verify pushed events are consumed.
- Verify failures do not permanently break the pipeline.

## Phase 3: Modernization

- [x] Replace deprecated supervisor child specs from `Supervisor.Spec` with modern child specs.
- [x] Review dependency versions and remove unused dependencies.
- `mix.lock` contains packages not listed in `mix.exs`, suggesting old development dependencies or stale lock entries.
- Confirm whether `mix_test_watch`, `gen_stage`, `finch`, and `goth` are still the right choices.
- [x] Update configuration style for modern Elixir.
- Replace `use Mix.Config` with `import Config` when the supported Elixir version allows it.
- Move environment-specific configuration into explicit files only when needed.
- [x] Add static analysis once the test baseline is in place.
- Consider Credo for style/readability.
- Consider Dialyzer after typespecs and return contracts are clarified.
- [x] Improve module docs and typespecs for public functions.
- [x] Review package metadata before any Hex release.
- Confirm maintainers, links, versioning, changelog, and documentation generation.

## Phase 4: Future Work

- [ ] Define the Phase 4 public API before adding implementation.
- Decide whether queued delivery should be exposed as `Rocket.push_async/1`, `Rocket.push_many/1`, a supervised worker API, or a separate module.
- Keep `Rocket.push/1` as the synchronous API unless a breaking change is explicitly planned.
- Document return contracts, failure semantics, ordering guarantees, and backpressure behaviour before implementation.
- [ ] Add payload validation helpers for FCM HTTP v1 messages.
- Validate that payloads include a `message` object and at least one supported target such as `token`, `topic`, or `condition`.
- Validate common notification, data, Android, APNs, and webpush shapes without trying to replace Firebase's full server-side validation.
- Return structured validation errors before encoding/posting.
- [ ] Add a first-class batch API.
- Support sending many messages while preserving per-message results.
- Decide whether batching should be sequential, concurrent with a bounded concurrency limit, or queued through GenStage.
- Include tests for partial success, partial failure, ordering, and backpressure.
- [ ] Design retry and rate-limit handling.
- Add configurable retry policy for retryable HTTP/client errors such as timeouts, 429, 500, and 503.
- Respect FCM retry hints if present in response headers or body.
- Ensure non-retryable validation/auth errors fail fast.
- Include tests for retry count, delay calculation, and eventual failure.
- [ ] Add request options without global configuration mutation.
- Allow per-call overrides for response handler, timeout, retry policy, config provider, HTTP client, and telemetry metadata where appropriate.
- Keep defaults in application config for normal use.
- [ ] Improve credential source support.
- Support service-account JSON from application config, environment variables, and file paths.
- Evaluate whether a supervised Goth token server should replace per-request token fetching.
- Document token caching, refresh behaviour, and deployment recommendations.
- [ ] Add telemetry instrumentation.
- Emit events for request start/stop/exception, FCM status codes, retry attempts, queue depth, and pusher failures.
- Keep logs as a default handler, but make telemetry the primary integration point for observability.
- [ ] Clarify and improve the queued delivery pipeline.
- Decide whether GenStage remains necessary or whether `Task.Supervisor`/Broadway is a better fit.
- Add configurable queue limits and overflow strategy.
- Add graceful shutdown/drain behaviour for queued messages.
- [ ] Add richer response types.
- Convert common FCM error responses into named error structs or tagged tuples.
- Preserve raw response details where useful for debugging.
- Document compatibility guarantees for response shapes.
- [ ] Add integration tests that can run explicitly against Firebase.
- Keep live tests disabled by default.
- Require opt-in environment variables and clear documentation.
- Ensure normal CI remains deterministic and does not require GCP credentials.
- [ ] Prepare for a Hex release.
- Decide the next version number and whether API changes are breaking.
- Finalize changelog entries, generated docs, package files, and release checklist.
- Consider adding examples for Phoenix apps, releases, and supervised production usage.
7 changes: 4 additions & 3 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
import Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
Expand Down Expand Up @@ -30,6 +28,9 @@ use Mix.Config
# import_config "#{Mix.env}.exs"

config :rocket,
config_provider: Rocket.Config,
http_client: Rocket.HTTPClient.Finch,
request_module: Rocket.Request,
response_handler: Rocket.Response.DefaultHandler,
workers: 2

Expand Down
8 changes: 5 additions & 3 deletions lib/rocket.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
defmodule Rocket do
@moduledoc """
The Worker of Push Client for Exq
PushWorker can be used to issue Firebase Downstream Messages.
Firebase Cloud Messaging HTTP v1 client.
"""

alias Rocket.Request
require Logger

@doc """
Sends a single payload to FCM synchronously.
"""
@spec push(term()) :: {:ok, map()} | {:error, term()}
def push(payload) do
Request.perform(payload)
end
Expand Down
28 changes: 21 additions & 7 deletions lib/rocket/application.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
defmodule Rocket.Application do
@moduledoc false

use Application
import Supervisor.Spec

def start(_type, _args) do
workers = Application.get_env(:rocket, :workers, 2)
children = [worker(Rocket.PushCollector, [0])] |> add_worker(workers, 1)
children = finch_children() ++ [Rocket.PushCollector] ++ pusher_children(workers)

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Rocket.Supervisor]
Supervisor.start_link(children, opts)
end

defp add_worker(workers, amount, current) when amount == current, do: workers ++ [worker_id(current)]
defp add_worker(workers, amount, current), do: (workers ++ [worker_id(current)]) |> add_worker(amount, current + 1)
defp finch_children do
if http_client() == Rocket.HTTPClient.Finch and Code.ensure_loaded?(Finch) do
[{Finch, name: Application.get_env(:rocket, :finch_pool, Rocket.Finch)}]
else
[]
end
end

defp pusher_children(workers) when workers > 0 do
for worker_id <- 1..workers do
Supervisor.child_spec({Rocket.Pusher, []}, id: {Rocket.Pusher, worker_id})
end
end

defp pusher_children(_workers), do: []

defp worker_id(id), do: worker(Rocket.Pusher, [], id: id)
defp http_client do
Application.get_env(:rocket, :http_client, Rocket.HTTPClient.Finch)
end
end
Loading
Loading