+1
-3
.github/workflows/code-style.yml
+1
-3
.github/workflows/code-style.yml
+1
-3
.github/workflows/tests.yml
+1
-3
.github/workflows/tests.yml
+2
.gitignore
+2
.gitignore
+67
CONTRIBUTING.md
+67
CONTRIBUTING.md
···
1
+
# Contributing
2
+
3
+
Contributions are **welcome** and will be fully **credited**.
4
+
5
+
## Etiquette
6
+
7
+
This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work.
8
+
9
+
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people.
10
+
11
+
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
12
+
13
+
## Viability
14
+
15
+
When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project.
16
+
17
+
## Procedure
18
+
19
+
### Before Filing an Issue
20
+
21
+
- Search existing issues to avoid duplicates
22
+
- Check the [documentation](docs/) to ensure it's not a usage question
23
+
- Provide a clear title and description
24
+
- Include steps to reproduce the issue
25
+
- Specify your environment (PHP version, Laravel version, Signal version, mode)
26
+
- Include relevant code samples and full error messages
27
+
28
+
### Before Submitting a Pull Request
29
+
30
+
- **Discuss non-trivial changes first** by opening an issue
31
+
- **Fork the repository** and create a feature branch from `main`
32
+
- **Follow all requirements** listed below
33
+
- **Write tests** for your changes
34
+
- **Update documentation** if behavior changes
35
+
- **Run code style checks** with `vendor/bin/php-cs-fixer fix`
36
+
- **Ensure all tests pass** with `vendor/bin/phpunit`
37
+
- **Write clear commit messages** that explain what and why
38
+
39
+
## Requirements
40
+
41
+
- **[PSR-12 Coding Standard](https://www.php-fig.org/psr/psr-12/)** - Run `vendor/bin/php-cs-fixer fix` to automatically fix code style issues.
42
+
43
+
- **Add tests** - Your patch won't be accepted if it doesn't have tests. All tests must use [PHPUnit](https://phpunit.de/).
44
+
45
+
- **Document any change in behaviour** - Make sure the `README.md`, `docs/`, and any other relevant documentation are kept up-to-date.
46
+
47
+
- **Consider our release cycle** - We follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
48
+
49
+
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
50
+
51
+
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
52
+
53
+
## Running Tests
54
+
55
+
```bash
56
+
vendor/bin/phpunit
57
+
```
58
+
59
+
## Code Style
60
+
61
+
Signal follows PSR-12 coding standard. Run PHP CS Fixer before submitting:
62
+
63
+
```bash
64
+
vendor/bin/php-cs-fixer fix
65
+
```
66
+
67
+
**Happy coding**!
+114
-739
README.md
+114
-739
README.md
···
1
-
# Signal
1
+
[](https://github.com/socialdept/atp-signals)
2
2
3
-
**Laravel package for building Signals that respond to AT Protocol events**
3
+
<h3 align="center">
4
+
Consume real-time AT Protocol events in your Laravel application.
5
+
</h3>
4
6
5
-
Signal provides a clean, Laravel-style interface for consuming real-time events from the AT Protocol. Supports both **Jetstream** (simplified JSON events) and **Firehose** (raw CBOR/CAR format) for maximum flexibility. Build reactive applications, AppViews, and custom indexers that respond to posts, likes, follows, and other social interactions on the AT Protocol network.
7
+
<p align="center">
8
+
<br>
9
+
<a href="https://packagist.org/packages/socialdept/atp-signals" title="Latest Version on Packagist"><img src="https://img.shields.io/packagist/v/socialdept/atp-signals.svg?style=flat-square"></a>
10
+
<a href="https://packagist.org/packages/socialdept/atp-signals" title="Total Downloads"><img src="https://img.shields.io/packagist/dt/socialdept/atp-signals.svg?style=flat-square"></a>
11
+
<a href="https://github.com/socialdept/atp-signals/actions/workflows/tests.yml" title="GitHub Tests Action Status"><img src="https://img.shields.io/github/actions/workflow/status/socialdept/atp-signals/tests.yml?branch=main&label=tests&style=flat-square"></a>
12
+
<a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-signals?style=flat-square"></a>
13
+
</p>
6
14
7
15
---
8
16
9
-
## Features
17
+
## What is Signal?
10
18
11
-
- ๐ **Dual-Mode Support** - Choose between Jetstream (JSON) or Firehose (CBOR/CAR) based on your needs
12
-
- ๐ **WebSocket Connection** - Connect to AT Protocol with automatic reconnection and exponential backoff
13
-
- ๐ฏ **Signal-based Architecture** - Clean, testable event handlers (avoiding Laravel's "listener" naming collision)
14
-
- โญ **Wildcard Collection Filtering** - Match multiple collections with patterns like `app.bsky.feed.*`
15
-
- ๐๏ธ **AppView Ready** - Full support for custom collections and building AT Protocol AppViews
16
-
- ๐พ **Cursor Management** - Resume from last position after disconnections (Database, Redis, or File storage)
17
-
- โก **Queue Integration** - Process events asynchronously with Laravel queues
18
-
- ๐ **Auto-Discovery** - Automatically find and register Signals in `app/Signals`
19
-
- ๐งช **Testing Tools** - Test your Signals with sample data
20
-
- ๐ ๏ธ **Artisan Commands** - Full CLI support for managing and testing Signals
19
+
**Signal** is a Laravel package that lets you respond to real-time events from the AT Protocol network. Build reactive applications, custom feeds, moderation tools, analytics systems, and AppViews by listening to posts, likes, follows, and other social interactions as they happen across Bluesky and the entire AT Protocol ecosystem.
21
20
22
-
---
21
+
Think of it as Laravel's event listeners, but for the decentralized social web.
23
22
24
-
## Table of Contents
23
+
## Why use Signal?
25
24
26
-
<!-- TOC -->
27
-
* [Installation](#installation)
28
-
* [Quick Start](#quick-start)
29
-
* [Jetstream vs Firehose](#jetstream-vs-firehose)
30
-
* [Creating Signals](#creating-signals)
31
-
* [Filtering Events](#filtering-events)
32
-
* [Queue Integration](#queue-integration)
33
-
* [Configuration](#configuration-1)
34
-
* [Programmatic Usage](#programmatic-usage)
35
-
* [Available Commands](#available-commands)
36
-
* [Testing](#testing)
37
-
* [External Resources](#external-resources)
38
-
* [Examples](#examples)
39
-
* [Requirements](#requirements)
40
-
* [License](#license)
41
-
* [Support](#support)
42
-
<!-- TOC -->
43
-
44
-
---
45
-
46
-
## Installation
47
-
48
-
Install the package via Composer:
25
+
- **Laravel-style code** - Familiar patterns you already know
26
+
- **Real-time processing** - React to events as they happen
27
+
- **Dual-mode support** - Choose Jetstream (efficient JSON) or Firehose (comprehensive CBOR)
28
+
- **AppView ready** - Full support for custom collections and protocols
29
+
- **Production features** - Queue integration, cursor management, auto-reconnection
30
+
- **Easy filtering** - Target specific collections, operations, and users with wildcards
31
+
- **Built-in testing** - Test your signals with sample data
49
32
50
-
```bash
51
-
composer require socialdept/signal
52
-
```
53
-
54
-
Run the installation command:
55
-
56
-
```bash
57
-
php artisan signal:install
58
-
```
59
-
60
-
This will:
61
-
- Publish the configuration file to `config/signal.php`
62
-
- Publish the database migration
63
-
- Run migrations (with confirmation)
64
-
- Display next steps
65
-
66
-
### Manual Installation
67
-
68
-
If you prefer manual installation:
69
-
70
-
```bash
71
-
php artisan vendor:publish --tag=signal-config
72
-
php artisan vendor:publish --tag=signal-migrations
73
-
php artisan migrate
74
-
```
75
-
76
-
---
77
-
78
-
## Quick Start
79
-
80
-
### 1. Create Your First Signal
81
-
82
-
```bash
83
-
php artisan make:signal NewPostSignal
84
-
```
85
-
86
-
This creates `app/Signals/NewPostSignal.php`:
33
+
## Quick Example
87
34
88
35
```php
89
-
<?php
90
-
91
-
namespace App\Signals;
92
-
93
-
use SocialDept\Signal\Events\SignalEvent;
94
-
use SocialDept\Signal\Signals\Signal;
36
+
use SocialDept\AtpSignals\Events\SignalEvent;
37
+
use SocialDept\AtpSignals\Signals\Signal;
95
38
96
39
class NewPostSignal extends Signal
97
40
{
···
117
60
}
118
61
```
119
62
120
-
### 2. Start Consuming Events
121
-
122
-
```bash
123
-
php artisan signal:consume
124
-
```
125
-
126
-
Your Signal will now respond to new posts on the AT Protocol network in real-time!
127
-
128
-
---
63
+
Run `php artisan signal:consume` and start responding to every post on Bluesky in real-time.
129
64
130
-
## Jetstream vs Firehose
131
-
132
-
Signal supports two modes for consuming AT Protocol events. Choose based on your use case:
133
-
134
-
### Jetstream Mode (Default)
135
-
136
-
**Best for**: Standard Bluesky collections, production efficiency, lower bandwidth
65
+
## Installation
137
66
138
67
```bash
139
-
php artisan signal:consume --mode=jetstream
68
+
composer require socialdept/atp-signals
69
+
php artisan signal:install
140
70
```
141
71
142
-
**Characteristics:**
143
-
- โ
Simplified JSON events (easy to work with)
144
-
- โ
Server-side collection filtering (efficient)
145
-
- โ
Lower bandwidth and processing overhead
146
-
- โ ๏ธ Only standard `app.bsky.*` collections get create/update operations
147
-
- โ ๏ธ Custom collections only receive delete operations
72
+
That's it. [Read the installation docs โ](docs/installation.md)
148
73
149
-
**Jetstream URL options:**
150
-
- US East: `wss://jetstream2.us-east.bsky.network` (default)
151
-
- US West: `wss://jetstream1.us-west.bsky.network`
74
+
## Getting Started
152
75
153
-
### Firehose Mode
76
+
Once installed, you're three steps away from consuming AT Protocol events:
154
77
155
-
**Best for**: Custom collections, AppViews, comprehensive indexing
78
+
### 1. Create a Signal
156
79
157
80
```bash
158
-
php artisan signal:consume --mode=firehose
81
+
php artisan make:signal NewPostSignal
159
82
```
160
83
161
-
**Characteristics:**
162
-
- โ
**All operations** (create, update, delete) for **all collections**
163
-
- โ
Perfect for custom collections (e.g., `app.yourapp.*.collection`)
164
-
- โ
Full CBOR/CAR decoding with package `revolution/laravel-bluesky`
165
-
- โ ๏ธ Client-side filtering only (higher bandwidth)
166
-
- โ ๏ธ More processing overhead
167
-
168
-
**When to use Firehose:**
169
-
- Building an AT Protocol AppView
170
-
- Working with custom collections
171
-
- Need create/update events for non-standard collections
172
-
- Building comprehensive indexes
173
-
174
-
### Configuration
175
-
176
-
Set your preferred mode in `.env`:
177
-
178
-
```env
179
-
# Use Jetstream (default)
180
-
SIGNAL_MODE=jetstream
181
-
182
-
# Or use Firehose for custom collections
183
-
SIGNAL_MODE=firehose
184
-
```
185
-
186
-
### Example: Custom Collections
187
-
188
-
If you're tracking custom collections like `app.offprint.beta.publication`, you **must** use Firehose mode:
84
+
### 2. Define What to Listen For
189
85
190
86
```php
191
-
class PublicationSignal extends Signal
87
+
public function collections(): ?array
192
88
{
193
-
public function collections(): ?array
194
-
{
195
-
return ['app.offprint.beta.publication'];
196
-
}
197
-
198
-
public function handle(SignalEvent $event): void
199
-
{
200
-
// With Jetstream: Only sees deletes โ
201
-
// With Firehose: Sees creates, updates, deletes โ
202
-
}
89
+
return ['app.bsky.feed.post'];
203
90
}
204
91
```
205
92
206
-
---
93
+
### 3. Start Consuming
207
94
208
-
## Creating Signals
95
+
```bash
96
+
php artisan signal:consume
97
+
```
209
98
210
-
### Basic Signal Structure
99
+
Your Signal will now handle every matching event from the network. [Read the quickstart guide โ](docs/quickstart.md)
211
100
212
-
Every Signal extends the base `Signal` class and must implement:
101
+
## What can you build?
213
102
214
-
```php
215
-
use SocialDept\Signal\Enums\SignalEventType;
216
-
use SocialDept\Signal\Events\SignalEvent;
217
-
use SocialDept\Signal\Signals\Signal;
103
+
- **Custom feeds** - Curate content based on your own algorithms
104
+
- **Moderation tools** - Detect and flag problematic content automatically
105
+
- **Analytics platforms** - Track engagement, trends, and network growth
106
+
- **Social integrations** - Mirror content to other platforms in real-time
107
+
- **Notification systems** - Alert users about relevant activity
108
+
- **AppViews** - Build custom AT Protocol applications with your own collections
218
109
219
-
class MySignal extends Signal
220
-
{
221
-
// Required: Define which event types to listen for
222
-
public function eventTypes(): array
223
-
{
224
-
return [SignalEventType::Commit];
110
+
## Documentation
225
111
226
-
// Or use strings:
227
-
// return ['commit'];
228
-
}
112
+
**Getting Started**
113
+
- [Installation](docs/installation.md) - Detailed setup instructions
114
+
- [Quickstart Guide](docs/quickstart.md) - Build your first Signal
115
+
- [Jetstream vs Firehose](docs/modes.md) - Choose the right mode
229
116
230
-
// Required: Handle the event
231
-
public function handle(SignalEvent $event): void
232
-
{
233
-
// Your logic here
234
-
}
235
-
}
236
-
```
117
+
**Building Signals**
118
+
- [Creating Signals](docs/signals.md) - Complete Signal reference
119
+
- [Filtering Events](docs/filtering.md) - Target specific collections and operations
120
+
- [Queue Integration](docs/queues.md) - Process events asynchronously
237
121
238
-
**Enums vs Strings**: Signal supports both typed enums and strings for better IDE support and type safety. Use whichever you prefer!
239
-
240
-
### Event Types
241
-
242
-
Three event types are available:
243
-
244
-
| Enum | String | Description | Use Cases |
245
-
|-----------------------------|--------------|--------------------------------------------------|---------------------------------------|
246
-
| `SignalEventType::Commit` | `'commit'` | Repository commits (posts, likes, follows, etc.) | Content creation, social interactions |
247
-
| `SignalEventType::Identity` | `'identity'` | Identity changes (handle updates) | User profile tracking |
248
-
| `SignalEventType::Account` | `'account'` | Account status changes | Account monitoring |
122
+
**Advanced**
123
+
- [Configuration](docs/configuration.md) - All config options explained
124
+
- [Testing](docs/testing.md) - Test your Signals
125
+
- [Examples](docs/examples.md) - Real-world use cases
249
126
250
-
### Accessing Event Data
127
+
## Example Use Cases
251
128
129
+
### Track User Growth
252
130
```php
253
-
use SocialDept\Signal\Enums\SignalCommitOperation;
254
-
255
-
public function handle(SignalEvent $event): void
131
+
public function collections(): ?array
256
132
{
257
-
// Common properties
258
-
$did = $event->did; // User's DID
259
-
$kind = $event->kind; // Event type
260
-
$timestamp = $event->timeUs; // Microsecond timestamp
261
-
262
-
// Commit events
263
-
if ($event->isCommit()) {
264
-
$collection = $event->getCollection(); // e.g., 'app.bsky.feed.post'
265
-
$operation = $event->getOperation(); // SignalCommitOperation enum
266
-
$record = $event->getRecord(); // The actual record data
267
-
$rkey = $event->commit->rkey; // Record key
268
-
269
-
// Use enum for type-safe comparisons
270
-
if ($operation === SignalCommitOperation::Create) {
271
-
// Handle new records
272
-
}
273
-
274
-
// Or get string value
275
-
$operationString = $operation->value; // 'create', 'update', or 'delete'
276
-
}
277
-
278
-
// Identity events
279
-
if ($event->isIdentity()) {
280
-
$handle = $event->identity->handle;
281
-
}
282
-
283
-
// Account events
284
-
if ($event->isAccount()) {
285
-
$active = $event->account->active;
286
-
$status = $event->account->status;
287
-
}
133
+
return ['app.bsky.graph.follow'];
288
134
}
289
135
```
290
136
291
-
---
292
-
293
-
## Filtering Events
294
-
295
-
### Collection Filtering (with Wildcards!)
296
-
297
-
Filter events by AT Protocol collection.
298
-
299
-
**Important**:
300
-
- **Jetstream mode**: Exact collection names are sent as URL parameters for server-side filtering. Wildcards work for client-side filtering only.
301
-
- **Firehose mode**: All filtering is client-side. Wildcards work normally.
302
-
137
+
### Monitor Content Moderation
303
138
```php
304
-
// Exact match - only posts
305
-
public function collections(): ?array
306
-
{
307
-
return ['app.bsky.feed.post'];
308
-
}
309
-
310
-
// Wildcard - all feed events
311
139
public function collections(): ?array
312
140
{
313
141
return ['app.bsky.feed.*'];
314
142
}
315
143
316
-
// Multiple patterns
317
-
public function collections(): ?array
144
+
public function shouldQueue(): bool
318
145
{
319
-
return [
320
-
'app.bsky.feed.post',
321
-
'app.bsky.feed.repost',
322
-
'app.bsky.graph.*', // All graph collections
323
-
];
324
-
}
325
-
326
-
// No filter - all collections
327
-
public function collections(): ?array
328
-
{
329
-
return null;
146
+
return true; // Process in background
330
147
}
331
148
```
332
149
333
-
### Common Collection Patterns
334
-
335
-
| Pattern | Matches |
336
-
|--------------------|-----------------------------|
337
-
| `app.bsky.feed.*` | Posts, likes, reposts, etc. |
338
-
| `app.bsky.graph.*` | Follows, blocks, mutes |
339
-
| `app.bsky.actor.*` | Profile updates |
340
-
| `app.bsky.*` | All Bluesky collections |
341
-
342
-
### Operation Filtering
343
-
344
-
Filter events by operation type (only applies to `commit` events):
345
-
150
+
### Build Custom Collections (AppView)
346
151
```php
347
-
use SocialDept\Signal\Enums\SignalCommitOperation;
348
-
349
-
// Only handle creates (using enum)
350
-
public function operations(): ?array
152
+
public function collections(): ?array
351
153
{
352
-
return [SignalCommitOperation::Create];
353
-
}
354
-
355
-
// Only handle creates and updates (using enums)
356
-
public function operations(): ?array
357
-
{
358
-
return [
359
-
SignalCommitOperation::Create,
360
-
SignalCommitOperation::Update,
361
-
];
362
-
}
363
-
364
-
// Only handle deletes (using string)
365
-
public function operations(): ?array
366
-
{
367
-
return ['delete'];
368
-
}
369
-
370
-
// No filter - all operations (default)
371
-
public function operations(): ?array
372
-
{
373
-
return null;
154
+
return ['app.yourapp.custom.collection'];
374
155
}
375
156
```
376
157
377
-
**Available operations:**
378
-
379
-
| Enum | String | Description |
380
-
|---------------------------------|------------|---------------------------|
381
-
| `SignalCommitOperation::Create` | `'create'` | New records created |
382
-
| `SignalCommitOperation::Update` | `'update'` | Existing records modified |
383
-
| `SignalCommitOperation::Delete` | `'delete'` | Records removed |
384
-
385
-
**Example use cases:**
386
-
```php
387
-
use SocialDept\Signal\Enums\SignalCommitOperation;
158
+
[See more examples โ](docs/examples.md)
388
159
389
-
// Signal that only handles new posts (not edits)
390
-
class NewPostSignal extends Signal
391
-
{
392
-
public function collections(): ?array
393
-
{
394
-
return ['app.bsky.feed.post'];
395
-
}
160
+
## Key Features Explained
396
161
397
-
public function operations(): ?array
398
-
{
399
-
return [SignalCommitOperation::Create];
400
-
}
401
-
}
162
+
### Jetstream vs Firehose
402
163
403
-
// Signal that only handles content updates
404
-
class ContentUpdateSignal extends Signal
405
-
{
406
-
public function collections(): ?array
407
-
{
408
-
return ['app.bsky.feed.post'];
409
-
}
164
+
Signal supports two modes for consuming AT Protocol events:
410
165
411
-
public function operations(): ?array
412
-
{
413
-
return [SignalCommitOperation::Update];
414
-
}
415
-
}
166
+
- **Jetstream** (default) - Simplified JSON events with server-side filtering
167
+
- **Firehose** - Raw CBOR/CAR format with client-side filtering
416
168
417
-
// Signal that handles deletions for cleanup
418
-
class CleanupSignal extends Signal
419
-
{
420
-
public function collections(): ?array
421
-
{
422
-
return ['app.bsky.feed.*'];
423
-
}
424
-
425
-
public function operations(): ?array
426
-
{
427
-
return [SignalCommitOperation::Delete];
428
-
}
429
-
}
430
-
```
169
+
[Learn more about modes โ](docs/modes.md)
431
170
432
-
### DID Filtering
171
+
### Wildcard Filtering
433
172
434
-
Filter events by specific users:
173
+
Match multiple collections with patterns:
435
174
436
175
```php
437
-
public function dids(): ?array
176
+
public function collections(): ?array
438
177
{
439
178
return [
440
-
'did:plc:z72i7hdynmk6r22z27h6tvur', // Specific user
441
-
'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // Another user
179
+
'app.bsky.feed.*', // All feed events
180
+
'app.bsky.graph.*', // All graph events
181
+
'app.yourapp.*', // All your custom collections
442
182
];
443
183
}
444
184
```
445
185
446
-
### Custom Filtering
186
+
[Learn more about filtering โ](docs/filtering.md)
447
187
448
-
Add complex filtering logic:
188
+
### Queue Integration
189
+
190
+
Process events asynchronously for better performance:
449
191
450
192
```php
451
-
public function shouldHandle(SignalEvent $event): bool
193
+
public function shouldQueue(): bool
452
194
{
453
-
// Only handle posts with images
454
-
if ($event->isCommit() && $event->commit->collection === 'app.bsky.feed.post') {
455
-
$record = $event->getRecord();
456
-
return isset($record->embed);
457
-
}
458
-
459
195
return true;
460
196
}
461
197
```
462
198
463
-
---
464
-
465
-
## Queue Integration
466
-
467
-
Process events asynchronously using Laravel queues:
468
-
469
-
```php
470
-
class HeavyProcessingSignal extends Signal
471
-
{
472
-
public function eventTypes(): array
473
-
{
474
-
return ['commit'];
475
-
}
476
-
477
-
// Enable queueing
478
-
public function shouldQueue(): bool
479
-
{
480
-
return true;
481
-
}
482
-
483
-
// Optional: Customize queue
484
-
public function queue(): string
485
-
{
486
-
return 'high-priority';
487
-
}
488
-
489
-
// Optional: Customize connection
490
-
public function queueConnection(): string
491
-
{
492
-
return 'redis';
493
-
}
494
-
495
-
public function handle(SignalEvent $event): void
496
-
{
497
-
// This runs in a queue job
498
-
$this->performExpensiveOperation($event);
499
-
}
500
-
501
-
// Handle failures
502
-
public function failed(SignalEvent $event, \Throwable $exception): void
503
-
{
504
-
Log::error('Signal failed', [
505
-
'event' => $event->toArray(),
506
-
'error' => $exception->getMessage(),
507
-
]);
508
-
}
509
-
}
510
-
```
511
-
512
-
---
513
-
514
-
## Configuration
515
-
516
-
Configuration is stored in `config/signal.php`:
517
-
518
-
### Consumer Mode
519
-
520
-
Choose between Jetstream (JSON) or Firehose (CBOR) mode:
521
-
522
-
```php
523
-
'mode' => env('SIGNAL_MODE', 'jetstream'),
524
-
```
525
-
526
-
Options:
527
-
- `jetstream` - JSON events, server-side filtering (default)
528
-
- `firehose` - CBOR events, client-side filtering (required for custom collections)
529
-
530
-
### Jetstream Configuration
531
-
532
-
```php
533
-
'websocket_url' => env('SIGNAL_JETSTREAM_URL', 'wss://jetstream2.us-east.bsky.network'),
534
-
```
535
-
536
-
Available endpoints:
537
-
- **US East**: `wss://jetstream2.us-east.bsky.network` (default)
538
-
- **US West**: `wss://jetstream1.us-west.bsky.network`
539
-
540
-
### Firehose Configuration
541
-
542
-
```php
543
-
'firehose' => [
544
-
'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'),
545
-
],
546
-
```
547
-
548
-
The raw firehose endpoint is: `wss://{host}/xrpc/com.atproto.sync.subscribeRepos`
549
-
550
-
### Cursor Storage
551
-
552
-
Choose how to store cursor positions:
553
-
554
-
```php
555
-
'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'),
556
-
```
557
-
558
-
| Driver | Best For | Configuration |
559
-
|------------|-------------------------------|--------------------|
560
-
| `database` | Production, multi-server | Default connection |
561
-
| `redis` | High performance, distributed | Redis connection |
562
-
| `file` | Development, single server | Storage path |
563
-
564
-
### Environment Variables
565
-
566
-
Add to your `.env`:
567
-
568
-
```env
569
-
# Consumer Mode
570
-
SIGNAL_MODE=jetstream # or 'firehose' for custom collections
571
-
572
-
# Jetstream Configuration
573
-
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
574
-
575
-
# Firehose Configuration (only needed if using firehose mode)
576
-
SIGNAL_FIREHOSE_HOST=bsky.network
577
-
578
-
# Optional Configuration
579
-
SIGNAL_CURSOR_STORAGE=database
580
-
SIGNAL_QUEUE_CONNECTION=redis
581
-
SIGNAL_QUEUE=signal
582
-
SIGNAL_BATCH_SIZE=100
583
-
SIGNAL_RATE_LIMIT=1000
584
-
```
585
-
586
-
### Auto-Discovery
587
-
588
-
Signals are automatically discovered from `app/Signals`. Disable if needed:
589
-
590
-
```php
591
-
'auto_discovery' => [
592
-
'enabled' => true,
593
-
'path' => app_path('Signals'),
594
-
'namespace' => 'App\\Signals',
595
-
],
596
-
```
597
-
598
-
Or manually register Signals:
599
-
600
-
```php
601
-
'signals' => [
602
-
\App\Signals\NewPostSignal::class,
603
-
\App\Signals\NewFollowSignal::class,
604
-
],
605
-
```
606
-
607
-
---
608
-
609
-
## Programmatic Usage
610
-
611
-
You can start and stop the consumer programmatically using the `Signal` facade:
612
-
613
-
```php
614
-
use SocialDept\Signal\Facades\Signal;
615
-
616
-
// Start consuming events (uses mode from config)
617
-
Signal::start();
618
-
619
-
// Start from a specific cursor
620
-
Signal::start(cursor: 123456789);
621
-
622
-
// Check which mode is active
623
-
$mode = Signal::getMode(); // Returns 'jetstream' or 'firehose'
624
-
625
-
// Stop consuming events
626
-
Signal::stop();
627
-
```
628
-
629
-
The facade automatically resolves the correct consumer (Jetstream or Firehose) based on your `config('signal.mode')` setting. This allows you to:
630
-
631
-
- Switch between modes by changing configuration
632
-
- Start consumers from application code (e.g., in a custom command)
633
-
- Integrate Signal into existing application workflows
634
-
635
-
```php
636
-
// Example: Start consumer based on environment
637
-
if (app()->environment('production')) {
638
-
config(['signal.mode' => 'jetstream']); // Use efficient Jetstream
639
-
} else {
640
-
config(['signal.mode' => 'firehose']); // Use comprehensive Firehose for testing
641
-
}
642
-
643
-
Signal::start();
644
-
```
645
-
646
-
---
199
+
[Learn more about queues โ](docs/queues.md)
647
200
648
201
## Available Commands
649
202
650
-
### `signal:install`
651
-
Install the package (publish config, migrations, run migrations)
652
-
653
203
```bash
204
+
# Install Signal
654
205
php artisan signal:install
655
-
```
656
206
657
-
### `signal:consume`
658
-
Start consuming events from AT Protocol
659
-
660
-
```bash
661
-
# Use default mode from config
662
-
php artisan signal:consume
663
-
664
-
# Override mode
665
-
php artisan signal:consume --mode=jetstream
666
-
php artisan signal:consume --mode=firehose
667
-
668
-
# Start from specific cursor
669
-
php artisan signal:consume --cursor=123456789
207
+
# Create a new Signal
208
+
php artisan make:signal YourSignal
670
209
671
-
# Start fresh (ignore stored cursor)
672
-
php artisan signal:consume --fresh
673
-
674
-
# Combine options
675
-
php artisan signal:consume --mode=firehose --fresh
676
-
```
677
-
678
-
### `signal:list`
679
-
List all registered Signals
680
-
681
-
```bash
210
+
# List all registered Signals
682
211
php artisan signal:list
683
-
```
684
212
685
-
### `signal:make`
686
-
Create a new Signal class
687
-
688
-
```bash
689
-
php artisan make:signal NewPostSignal
213
+
# Start consuming events
214
+
php artisan signal:consume
690
215
691
-
# With options
692
-
php artisan make:signal FollowSignal --type=commit --collection=app.bsky.graph.follow
216
+
# Test a Signal with sample data
217
+
php artisan signal:test YourSignal
693
218
```
694
219
695
-
### `signal:test`
696
-
Test a Signal with sample data
220
+
## Requirements
697
221
698
-
```bash
699
-
php artisan signal:test NewPostSignal
700
-
```
222
+
- PHP 8.2+
223
+
- Laravel 11+
224
+
- WebSocket support (enabled by default)
701
225
702
-
---
703
-
704
-
## Testing
705
-
706
-
Signal includes a comprehensive test suite. Test your Signals:
707
-
708
-
### Unit Testing
709
-
710
-
```php
711
-
use SocialDept\Signal\Events\CommitEvent;
712
-
use SocialDept\Signal\Events\SignalEvent;
713
-
714
-
class NewPostSignalTest extends TestCase
715
-
{
716
-
/** @test */
717
-
public function it_handles_new_posts()
718
-
{
719
-
$signal = new NewPostSignal();
720
-
721
-
$event = new SignalEvent(
722
-
did: 'did:plc:test',
723
-
timeUs: time() * 1000000,
724
-
kind: 'commit',
725
-
commit: new CommitEvent(
726
-
rev: 'test',
727
-
operation: 'create',
728
-
collection: 'app.bsky.feed.post',
729
-
rkey: 'test',
730
-
record: (object) [
731
-
'text' => 'Hello World!',
732
-
'createdAt' => now()->toIso8601String(),
733
-
],
734
-
),
735
-
);
736
-
737
-
$signal->handle($event);
738
-
739
-
// Assert your expected behavior
740
-
}
741
-
}
742
-
```
743
-
744
-
### Testing with Artisan
745
-
746
-
```bash
747
-
php artisan signal:test NewPostSignal
748
-
```
749
-
750
-
---
751
-
752
-
## External Resources
226
+
## Resources
753
227
754
228
- [AT Protocol Documentation](https://atproto.com/)
229
+
- [Bluesky API Docs](https://docs.bsky.app/)
755
230
- [Firehose Documentation](https://docs.bsky.app/docs/advanced-guides/firehose)
756
-
- [Bluesky Lexicon](https://atproto.com/lexicons)
231
+
- [Jetstream Documentation](https://github.com/bluesky-social/jetstream)
757
232
758
-
---
233
+
## Support & Contributing
759
234
760
-
## Examples
235
+
Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-signals/issues).
761
236
762
-
### Monitor All Feed Activity
237
+
Want to contribute? We'd love your help! Check out the [contribution guidelines](CONTRIBUTING.md).
763
238
764
-
```php
765
-
class FeedMonitorSignal extends Signal
766
-
{
767
-
public function eventTypes(): array
768
-
{
769
-
return ['commit'];
770
-
}
771
-
772
-
public function collections(): ?array
773
-
{
774
-
return ['app.bsky.feed.*'];
775
-
}
776
-
777
-
public function handle(SignalEvent $event): void
778
-
{
779
-
// Handles posts, likes, reposts, etc.
780
-
Log::info('Feed activity', [
781
-
'collection' => $event->getCollection(),
782
-
'operation' => $event->getOperation(),
783
-
'did' => $event->did,
784
-
]);
785
-
}
786
-
}
787
-
```
788
-
789
-
### Track New Follows
790
-
791
-
```php
792
-
class NewFollowSignal extends Signal
793
-
{
794
-
public function eventTypes(): array
795
-
{
796
-
return ['commit'];
797
-
}
798
-
799
-
public function collections(): ?array
800
-
{
801
-
return ['app.bsky.graph.follow'];
802
-
}
803
-
804
-
public function handle(SignalEvent $event): void
805
-
{
806
-
if ($event->commit->isCreate()) {
807
-
$record = $event->getRecord();
808
-
809
-
// Store follow relationship
810
-
Follow::create([
811
-
'follower_did' => $event->did,
812
-
'following_did' => $record->subject,
813
-
]);
814
-
}
815
-
}
816
-
}
817
-
```
818
-
819
-
### Content Moderation
239
+
## Credits
820
240
821
-
```php
822
-
class ModerationSignal extends Signal
823
-
{
824
-
public function eventTypes(): array
825
-
{
826
-
return ['commit'];
827
-
}
828
-
829
-
public function collections(): ?array
830
-
{
831
-
return ['app.bsky.feed.post'];
832
-
}
833
-
834
-
public function shouldQueue(): bool
835
-
{
836
-
return true;
837
-
}
838
-
839
-
public function handle(SignalEvent $event): void
840
-
{
841
-
$record = $event->getRecord();
842
-
843
-
if ($this->containsProhibitedContent($record->text)) {
844
-
$this->flagForModeration($event->did, $record);
845
-
}
846
-
}
847
-
}
848
-
```
849
-
850
-
---
851
-
852
-
## Requirements
853
-
854
-
- PHP 8.2 or higher
855
-
- Laravel 11.0 or higher
856
-
- WebSocket support (enabled by default in most environments)
857
-
858
-
---
241
+
- [Miguel Batres](https://batres.co) - founder & lead maintainer
242
+
- [All contributors](https://github.com/socialdept/atp-signals/graphs/contributors)
859
243
860
244
## License
861
245
862
-
The MIT License (MIT). Please see [LICENSE](LICENSE) for more information.
246
+
Signal is open-source software licensed under the [MIT license](LICENSE).
863
247
864
248
---
865
249
866
-
## Support
867
-
868
-
For issues, questions, or feature requests:
869
-
- Read the [README.md](./README.md) before opening issues
870
-
- Search through existing issues
871
-
- Open new issue
872
-
873
-
---
874
-
875
-
**Built for the AT Protocol ecosystem** โข Made with โค๏ธ by Social Dept
250
+
**Built for the Federation** โข By Social Dept.
+7
-6
composer.json
+7
-6
composer.json
···
1
1
{
2
-
"name": "socialdept/signal",
2
+
"name": "socialdept/atp-signals",
3
3
"description": "Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel",
4
4
"type": "library",
5
5
"license": "MIT",
···
14
14
},
15
15
"require-dev": {
16
16
"orchestra/testbench": "^9.0",
17
-
"phpunit/phpunit": "^11.0"
17
+
"phpunit/phpunit": "^11.0",
18
+
"friendsofphp/php-cs-fixer": "^3.89"
18
19
},
19
20
"autoload": {
20
21
"psr-4": {
21
-
"SocialDept\\Signal\\": "src/"
22
+
"SocialDept\\AtpSignals\\": "src/"
22
23
}
23
24
},
24
25
"autoload-dev": {
25
26
"psr-4": {
26
-
"SocialDept\\Signal\\Tests\\": "tests/"
27
+
"SocialDept\\AtpSignals\\Tests\\": "tests/"
27
28
}
28
29
},
29
30
"extra": {
30
31
"laravel": {
31
32
"providers": [
32
-
"SocialDept\\Signal\\SignalServiceProvider"
33
+
"SocialDept\\AtpSignals\\SignalServiceProvider"
33
34
],
34
35
"aliases": {
35
-
"Signal": "SocialDept\\Signal\\Facades\\Signal"
36
+
"Signal": "SocialDept\\AtpSignals\\Facades\\Signal"
36
37
}
37
38
}
38
39
}
+687
docs/configuration.md
+687
docs/configuration.md
···
1
+
# Configuration
2
+
3
+
Signal's configuration file provides complete control over how your application consumes AT Protocol events.
4
+
5
+
## Configuration File
6
+
7
+
After installation, configuration lives in `config/signal.php`.
8
+
9
+
### Publishing Configuration
10
+
11
+
Publish the config file manually if needed:
12
+
13
+
```bash
14
+
php artisan vendor:publish --tag=signal-config
15
+
```
16
+
17
+
This creates `config/signal.php` with all available options.
18
+
19
+
## Environment Variables
20
+
21
+
Most configuration can be set via `.env` for environment-specific values.
22
+
23
+
### Basic Configuration
24
+
25
+
```env
26
+
# Consumer Mode (jetstream or firehose)
27
+
SIGNAL_MODE=jetstream
28
+
29
+
# Jetstream Configuration
30
+
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
31
+
32
+
# Firehose Configuration
33
+
SIGNAL_FIREHOSE_HOST=bsky.network
34
+
35
+
# Cursor Storage (database, redis, or file)
36
+
SIGNAL_CURSOR_STORAGE=database
37
+
38
+
# Queue Configuration
39
+
SIGNAL_QUEUE_CONNECTION=redis
40
+
SIGNAL_QUEUE=signal
41
+
```
42
+
43
+
## Consumer Mode
44
+
45
+
Choose between Jetstream and Firehose mode.
46
+
47
+
### Configuration
48
+
49
+
```php
50
+
'mode' => env('SIGNAL_MODE', 'jetstream'),
51
+
```
52
+
53
+
**Options:**
54
+
- `jetstream` - JSON events, server-side filtering (default)
55
+
- `firehose` - CBOR/CAR events, client-side filtering
56
+
57
+
**Environment Variable:**
58
+
```env
59
+
SIGNAL_MODE=jetstream
60
+
```
61
+
62
+
**When to use each:**
63
+
- **Jetstream**: Standard Bluesky collections, production efficiency
64
+
- **Firehose**: Custom collections, AppViews, comprehensive indexing
65
+
66
+
[Learn more about modes โ](modes.md)
67
+
68
+
## Jetstream Configuration
69
+
70
+
Configuration specific to Jetstream mode.
71
+
72
+
### WebSocket URL
73
+
74
+
```php
75
+
'jetstream' => [
76
+
'websocket_url' => env(
77
+
'SIGNAL_JETSTREAM_URL',
78
+
'wss://jetstream2.us-east.bsky.network'
79
+
),
80
+
],
81
+
```
82
+
83
+
**Available Endpoints:**
84
+
85
+
**US East (Default):**
86
+
```env
87
+
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
88
+
```
89
+
90
+
**US West:**
91
+
```env
92
+
SIGNAL_JETSTREAM_URL=wss://jetstream1.us-west.bsky.network
93
+
```
94
+
95
+
Choose the endpoint closest to your server for best latency.
96
+
97
+
## Firehose Configuration
98
+
99
+
Configuration specific to Firehose mode.
100
+
101
+
### Host
102
+
103
+
```php
104
+
'firehose' => [
105
+
'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'),
106
+
],
107
+
```
108
+
109
+
The WebSocket URL is constructed as:
110
+
```
111
+
wss://{host}/xrpc/com.atproto.sync.subscribeRepos
112
+
```
113
+
114
+
**Environment Variable:**
115
+
```env
116
+
SIGNAL_FIREHOSE_HOST=bsky.network
117
+
```
118
+
119
+
**Default Host:** `bsky.network`
120
+
121
+
**Custom Hosts:** If you're running your own AT Protocol PDS, specify it here:
122
+
```env
123
+
SIGNAL_FIREHOSE_HOST=my-pds.example.com
124
+
```
125
+
126
+
## Cursor Storage
127
+
128
+
Configure how Signal stores cursor positions for resuming after disconnections.
129
+
130
+
### Storage Driver
131
+
132
+
```php
133
+
'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'),
134
+
```
135
+
136
+
**Available Drivers:**
137
+
- `database` - Store in database (recommended for production)
138
+
- `redis` - Store in Redis (high performance)
139
+
- `file` - Store in filesystem (development only)
140
+
141
+
**Environment Variable:**
142
+
```env
143
+
SIGNAL_CURSOR_STORAGE=database
144
+
```
145
+
146
+
### Database Driver
147
+
148
+
Uses Laravel's default database connection.
149
+
150
+
**Configuration:**
151
+
```php
152
+
'cursor_storage' => 'database',
153
+
```
154
+
155
+
**Requires:**
156
+
- Migration published and run
157
+
- Database connection configured
158
+
159
+
**Table:** `signal_cursors`
160
+
161
+
### Redis Driver
162
+
163
+
Stores cursors in Redis for high performance.
164
+
165
+
**Configuration:**
166
+
```php
167
+
'cursor_storage' => 'redis',
168
+
169
+
'redis' => [
170
+
'connection' => env('SIGNAL_REDIS_CONNECTION', 'default'),
171
+
'key_prefix' => env('SIGNAL_REDIS_PREFIX', 'signal:cursor:'),
172
+
],
173
+
```
174
+
175
+
**Environment Variables:**
176
+
```env
177
+
SIGNAL_CURSOR_STORAGE=redis
178
+
SIGNAL_REDIS_CONNECTION=default
179
+
SIGNAL_REDIS_PREFIX=signal:cursor:
180
+
```
181
+
182
+
**Requires:**
183
+
- Redis connection configured in `config/database.php`
184
+
- Redis server running
185
+
186
+
### File Driver
187
+
188
+
Stores cursors in the filesystem (development only).
189
+
190
+
**Configuration:**
191
+
```php
192
+
'cursor_storage' => 'file',
193
+
194
+
'file' => [
195
+
'path' => env('SIGNAL_FILE_PATH', storage_path('app/signal')),
196
+
],
197
+
```
198
+
199
+
**Environment Variables:**
200
+
```env
201
+
SIGNAL_CURSOR_STORAGE=file
202
+
SIGNAL_FILE_PATH=/path/to/storage/signal
203
+
```
204
+
205
+
**Not recommended for production:**
206
+
- Single server only
207
+
- No clustering support
208
+
- Filesystem I/O overhead
209
+
210
+
## Queue Configuration
211
+
212
+
Configure how Signal dispatches queued jobs.
213
+
214
+
### Queue Connection
215
+
216
+
```php
217
+
'queue' => [
218
+
'connection' => env('SIGNAL_QUEUE_CONNECTION', null),
219
+
'queue' => env('SIGNAL_QUEUE', 'default'),
220
+
],
221
+
```
222
+
223
+
**Environment Variables:**
224
+
```env
225
+
# Queue connection (redis, database, sqs, etc.)
226
+
SIGNAL_QUEUE_CONNECTION=redis
227
+
228
+
# Queue name
229
+
SIGNAL_QUEUE=signal
230
+
```
231
+
232
+
**Defaults:**
233
+
- `connection`: Uses Laravel's default queue connection
234
+
- `queue`: Uses Laravel's default queue name
235
+
236
+
### Per-Signal Configuration
237
+
238
+
Signals can override queue configuration:
239
+
240
+
```php
241
+
public function shouldQueue(): bool
242
+
{
243
+
return true;
244
+
}
245
+
246
+
public function queueConnection(): string
247
+
{
248
+
return 'redis'; // Override connection
249
+
}
250
+
251
+
public function queue(): string
252
+
{
253
+
return 'high-priority'; // Override queue name
254
+
}
255
+
```
256
+
257
+
[Learn more about queue integration โ](queues.md)
258
+
259
+
## Auto-Discovery
260
+
261
+
Configure automatic Signal discovery.
262
+
263
+
### Enable/Disable
264
+
265
+
```php
266
+
'auto_discovery' => [
267
+
'enabled' => true,
268
+
'path' => app_path('Signals'),
269
+
'namespace' => 'App\\Signals',
270
+
],
271
+
```
272
+
273
+
**Options:**
274
+
- `enabled`: Enable/disable auto-discovery (default: `true`)
275
+
- `path`: Directory to scan for Signals (default: `app/Signals`)
276
+
- `namespace`: Namespace for discovered Signals (default: `App\Signals`)
277
+
278
+
### Disable Auto-Discovery
279
+
280
+
Manually register Signals instead:
281
+
282
+
```php
283
+
'auto_discovery' => [
284
+
'enabled' => false,
285
+
],
286
+
287
+
'signals' => [
288
+
\App\Signals\NewPostSignal::class,
289
+
\App\Signals\NewFollowSignal::class,
290
+
],
291
+
```
292
+
293
+
### Custom Discovery Path
294
+
295
+
Organize Signals in a custom directory:
296
+
297
+
```php
298
+
'auto_discovery' => [
299
+
'enabled' => true,
300
+
'path' => app_path('Domain/Signals'),
301
+
'namespace' => 'App\\Domain\\Signals',
302
+
],
303
+
```
304
+
305
+
## Manual Signal Registration
306
+
307
+
Register Signals explicitly.
308
+
309
+
### Configuration
310
+
311
+
```php
312
+
'signals' => [
313
+
\App\Signals\NewPostSignal::class,
314
+
\App\Signals\NewFollowSignal::class,
315
+
\App\Signals\ProfileUpdateSignal::class,
316
+
],
317
+
```
318
+
319
+
**When to use:**
320
+
- Auto-discovery disabled
321
+
- Signals outside standard directory
322
+
- Fine-grained control over which Signals run
323
+
324
+
## Logging
325
+
326
+
Signal uses Laravel's logging system.
327
+
328
+
### Configure Logging
329
+
330
+
Standard Laravel log configuration applies:
331
+
332
+
```php
333
+
// config/logging.php
334
+
'channels' => [
335
+
'signal' => [
336
+
'driver' => 'daily',
337
+
'path' => storage_path('logs/signal.log'),
338
+
'level' => env('SIGNAL_LOG_LEVEL', 'debug'),
339
+
'days' => 14,
340
+
],
341
+
],
342
+
```
343
+
344
+
Use in Signals:
345
+
346
+
```php
347
+
use Illuminate\Support\Facades\Log;
348
+
349
+
public function handle(SignalEvent $event): void
350
+
{
351
+
Log::channel('signal')->info('Processing event', [
352
+
'did' => $event->did,
353
+
]);
354
+
}
355
+
```
356
+
357
+
## Complete Configuration Reference
358
+
359
+
Here's the full `config/signal.php` with all options:
360
+
361
+
```php
362
+
<?php
363
+
364
+
return [
365
+
366
+
/*
367
+
|--------------------------------------------------------------------------
368
+
| Consumer Mode
369
+
|--------------------------------------------------------------------------
370
+
|
371
+
| Choose between 'jetstream' (JSON events) or 'firehose' (CBOR/CAR events).
372
+
| Jetstream is more efficient for standard Bluesky collections.
373
+
| Firehose is required for custom collections.
374
+
|
375
+
| Options: 'jetstream', 'firehose'
376
+
|
377
+
*/
378
+
379
+
'mode' => env('SIGNAL_MODE', 'jetstream'),
380
+
381
+
/*
382
+
|--------------------------------------------------------------------------
383
+
| Jetstream Configuration
384
+
|--------------------------------------------------------------------------
385
+
|
386
+
| Configuration for Jetstream mode (JSON events).
387
+
|
388
+
*/
389
+
390
+
'jetstream' => [
391
+
'websocket_url' => env(
392
+
'SIGNAL_JETSTREAM_URL',
393
+
'wss://jetstream2.us-east.bsky.network'
394
+
),
395
+
],
396
+
397
+
/*
398
+
|--------------------------------------------------------------------------
399
+
| Firehose Configuration
400
+
|--------------------------------------------------------------------------
401
+
|
402
+
| Configuration for Firehose mode (CBOR/CAR events).
403
+
|
404
+
*/
405
+
406
+
'firehose' => [
407
+
'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'),
408
+
],
409
+
410
+
/*
411
+
|--------------------------------------------------------------------------
412
+
| Cursor Storage
413
+
|--------------------------------------------------------------------------
414
+
|
415
+
| Configure how Signal stores cursor positions for resuming after
416
+
| disconnections. Options: 'database', 'redis', 'file'
417
+
|
418
+
*/
419
+
420
+
'cursor_storage' => env('SIGNAL_CURSOR_STORAGE', 'database'),
421
+
422
+
/*
423
+
|--------------------------------------------------------------------------
424
+
| Redis Configuration
425
+
|--------------------------------------------------------------------------
426
+
|
427
+
| Configuration for Redis cursor storage.
428
+
|
429
+
*/
430
+
431
+
'redis' => [
432
+
'connection' => env('SIGNAL_REDIS_CONNECTION', 'default'),
433
+
'key_prefix' => env('SIGNAL_REDIS_PREFIX', 'signal:cursor:'),
434
+
],
435
+
436
+
/*
437
+
|--------------------------------------------------------------------------
438
+
| File Configuration
439
+
|--------------------------------------------------------------------------
440
+
|
441
+
| Configuration for file-based cursor storage.
442
+
|
443
+
*/
444
+
445
+
'file' => [
446
+
'path' => env('SIGNAL_FILE_PATH', storage_path('app/signal')),
447
+
],
448
+
449
+
/*
450
+
|--------------------------------------------------------------------------
451
+
| Queue Configuration
452
+
|--------------------------------------------------------------------------
453
+
|
454
+
| Configure queue connection and name for processing events asynchronously.
455
+
|
456
+
*/
457
+
458
+
'queue' => [
459
+
'connection' => env('SIGNAL_QUEUE_CONNECTION', null),
460
+
'queue' => env('SIGNAL_QUEUE', 'default'),
461
+
],
462
+
463
+
/*
464
+
|--------------------------------------------------------------------------
465
+
| Auto-Discovery
466
+
|--------------------------------------------------------------------------
467
+
|
468
+
| Automatically discover and register Signals from the specified directory.
469
+
|
470
+
*/
471
+
472
+
'auto_discovery' => [
473
+
'enabled' => true,
474
+
'path' => app_path('Signals'),
475
+
'namespace' => 'App\\Signals',
476
+
],
477
+
478
+
/*
479
+
|--------------------------------------------------------------------------
480
+
| Manual Signal Registration
481
+
|--------------------------------------------------------------------------
482
+
|
483
+
| Manually register Signals if auto-discovery is disabled.
484
+
|
485
+
*/
486
+
487
+
'signals' => [
488
+
// \App\Signals\NewPostSignal::class,
489
+
],
490
+
491
+
];
492
+
```
493
+
494
+
## Environment-Specific Configuration
495
+
496
+
### Development
497
+
498
+
```env
499
+
SIGNAL_MODE=firehose
500
+
SIGNAL_CURSOR_STORAGE=file
501
+
SIGNAL_QUEUE_CONNECTION=sync
502
+
```
503
+
504
+
**Why:**
505
+
- Firehose mode sees all events (comprehensive testing)
506
+
- File storage is simple and adequate
507
+
- Sync queue processes immediately (easier debugging)
508
+
509
+
### Staging
510
+
511
+
```env
512
+
SIGNAL_MODE=jetstream
513
+
SIGNAL_CURSOR_STORAGE=redis
514
+
SIGNAL_QUEUE_CONNECTION=redis
515
+
SIGNAL_QUEUE=signal-staging
516
+
```
517
+
518
+
**Why:**
519
+
- Jetstream mode matches production
520
+
- Redis for performance testing
521
+
- Separate queue for staging isolation
522
+
523
+
### Production
524
+
525
+
```env
526
+
SIGNAL_MODE=jetstream
527
+
SIGNAL_CURSOR_STORAGE=database
528
+
SIGNAL_QUEUE_CONNECTION=redis
529
+
SIGNAL_QUEUE=signal
530
+
```
531
+
532
+
**Why:**
533
+
- Jetstream mode for efficiency
534
+
- Database storage for reliability
535
+
- Redis queues for performance
536
+
537
+
## Runtime Configuration
538
+
539
+
Change configuration at runtime:
540
+
541
+
```php
542
+
use SocialDept\AtpSignals\Facades\Signal;
543
+
544
+
// Override mode
545
+
config(['signal.mode' => 'firehose']);
546
+
547
+
// Override cursor storage
548
+
config(['signal.cursor_storage' => 'redis']);
549
+
550
+
// Start consumer with new config
551
+
Signal::start();
552
+
```
553
+
554
+
## Validation
555
+
556
+
Signal validates configuration on startup:
557
+
558
+
```bash
559
+
php artisan signal:consume
560
+
```
561
+
562
+
**Checks:**
563
+
- Mode is valid (`jetstream` or `firehose`)
564
+
- Cursor storage driver exists
565
+
- Required endpoints are configured
566
+
- Queue configuration is valid
567
+
568
+
**Validation errors prevent consumer from starting.**
569
+
570
+
## Configuration Helpers
571
+
572
+
### Check Current Mode
573
+
574
+
```php
575
+
$mode = config('signal.mode'); // 'jetstream' or 'firehose'
576
+
```
577
+
578
+
Or via Facade:
579
+
580
+
```php
581
+
use SocialDept\AtpSignals\Facades\Signal;
582
+
583
+
$mode = Signal::getMode();
584
+
```
585
+
586
+
### Check Cursor Storage
587
+
588
+
```php
589
+
$storage = config('signal.cursor_storage'); // 'database', 'redis', or 'file'
590
+
```
591
+
592
+
### Check Queue Configuration
593
+
594
+
```php
595
+
$connection = config('signal.queue.connection');
596
+
$queue = config('signal.queue.queue');
597
+
```
598
+
599
+
## Best Practices
600
+
601
+
### Use Environment Variables
602
+
603
+
Don't hardcode values in config file:
604
+
605
+
```php
606
+
// Good
607
+
'mode' => env('SIGNAL_MODE', 'jetstream'),
608
+
609
+
// Bad
610
+
'mode' => 'jetstream',
611
+
```
612
+
613
+
### Separate Staging and Production
614
+
615
+
Use different queues and storage:
616
+
617
+
```env
618
+
# .env.staging
619
+
SIGNAL_QUEUE=signal-staging
620
+
621
+
# .env.production
622
+
SIGNAL_QUEUE=signal-production
623
+
```
624
+
625
+
### Document Custom Configuration
626
+
627
+
If you change defaults, document why:
628
+
629
+
```php
630
+
// We use Firehose mode because we have custom collections
631
+
'mode' => env('SIGNAL_MODE', 'firehose'),
632
+
```
633
+
634
+
### Version Control
635
+
636
+
Commit `config/signal.php` but not `.env`:
637
+
638
+
```gitignore
639
+
# .gitignore
640
+
.env
641
+
.env.*
642
+
643
+
# Commit
644
+
config/signal.php
645
+
```
646
+
647
+
## Troubleshooting
648
+
649
+
### Configuration Not Loading
650
+
651
+
Clear config cache:
652
+
653
+
```bash
654
+
php artisan config:clear
655
+
php artisan config:cache
656
+
```
657
+
658
+
### Environment Variables Not Working
659
+
660
+
Check `.env` file exists and is readable:
661
+
662
+
```bash
663
+
ls -la .env
664
+
```
665
+
666
+
Restart services after changing `.env`:
667
+
668
+
```bash
669
+
# If using Supervisor
670
+
sudo supervisorctl restart signal-consumer:*
671
+
```
672
+
673
+
### Invalid Configuration
674
+
675
+
Run consumer to see validation errors:
676
+
677
+
```bash
678
+
php artisan signal:consume
679
+
```
680
+
681
+
Signal will display specific errors about misconfiguration.
682
+
683
+
## Next Steps
684
+
685
+
- **[Learn about testing โ](testing.md)** - Test your configuration
686
+
- **[See real-world examples โ](examples.md)** - Learn from production configurations
687
+
- **[Review queue integration โ](queues.md)** - Configure queues optimally
+795
docs/examples.md
+795
docs/examples.md
···
1
+
# Real-World Examples
2
+
3
+
Learn from production-ready Signal examples covering common use cases.
4
+
5
+
## Social Media Analytics
6
+
7
+
Track engagement metrics across Bluesky.
8
+
9
+
```php
10
+
<?php
11
+
12
+
namespace App\Signals;
13
+
14
+
use App\Models\EngagementMetric;
15
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
16
+
use SocialDept\AtpSignals\Events\SignalEvent;
17
+
use SocialDept\AtpSignals\Signals\Signal;
18
+
use Illuminate\Support\Facades\DB;
19
+
20
+
class EngagementTrackerSignal extends Signal
21
+
{
22
+
public function eventTypes(): array
23
+
{
24
+
return ['commit'];
25
+
}
26
+
27
+
public function collections(): ?array
28
+
{
29
+
return [
30
+
'app.bsky.feed.post',
31
+
'app.bsky.feed.like',
32
+
'app.bsky.feed.repost',
33
+
'app.bsky.graph.follow',
34
+
];
35
+
}
36
+
37
+
public function operations(): ?array
38
+
{
39
+
return [SignalCommitOperation::Create];
40
+
}
41
+
42
+
public function shouldQueue(): bool
43
+
{
44
+
return true;
45
+
}
46
+
47
+
public function handle(SignalEvent $event): void
48
+
{
49
+
$collection = $event->getCollection();
50
+
$timestamp = $event->getTimestamp();
51
+
52
+
// Increment counter for this hour
53
+
DB::table('engagement_metrics')
54
+
->updateOrInsert(
55
+
[
56
+
'collection' => $collection,
57
+
'hour' => $timestamp->startOfHour(),
58
+
],
59
+
[
60
+
'count' => DB::raw('count + 1'),
61
+
'updated_at' => now(),
62
+
]
63
+
);
64
+
}
65
+
}
66
+
```
67
+
68
+
**Use case:** Build analytics dashboards showing posts/hour, likes/hour, follows/hour.
69
+
70
+
## Content Moderation
71
+
72
+
Automatically flag problematic content.
73
+
74
+
```php
75
+
<?php
76
+
77
+
namespace App\Signals;
78
+
79
+
use App\Models\FlaggedPost;
80
+
use App\Services\ModerationService;
81
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
82
+
use SocialDept\AtpSignals\Events\SignalEvent;
83
+
use SocialDept\AtpSignals\Signals\Signal;
84
+
85
+
class ModerationSignal extends Signal
86
+
{
87
+
public function __construct(
88
+
private ModerationService $moderation
89
+
) {}
90
+
91
+
public function eventTypes(): array
92
+
{
93
+
return ['commit'];
94
+
}
95
+
96
+
public function collections(): ?array
97
+
{
98
+
return ['app.bsky.feed.post'];
99
+
}
100
+
101
+
public function operations(): ?array
102
+
{
103
+
return [SignalCommitOperation::Create];
104
+
}
105
+
106
+
public function shouldQueue(): bool
107
+
{
108
+
return true;
109
+
}
110
+
111
+
public function queue(): string
112
+
{
113
+
return 'moderation';
114
+
}
115
+
116
+
public function handle(SignalEvent $event): void
117
+
{
118
+
$record = $event->getRecord();
119
+
120
+
if (!isset($record->text)) {
121
+
return;
122
+
}
123
+
124
+
$result = $this->moderation->analyze($record->text);
125
+
126
+
if ($result->needsReview) {
127
+
FlaggedPost::create([
128
+
'did' => $event->did,
129
+
'rkey' => $event->commit->rkey,
130
+
'text' => $record->text,
131
+
'reason' => $result->reason,
132
+
'confidence' => $result->confidence,
133
+
'flagged_at' => now(),
134
+
]);
135
+
}
136
+
}
137
+
138
+
public function failed(SignalEvent $event, \Throwable $exception): void
139
+
{
140
+
Log::error('Moderation signal failed', [
141
+
'did' => $event->did,
142
+
'error' => $exception->getMessage(),
143
+
]);
144
+
}
145
+
}
146
+
```
147
+
148
+
**Use case:** Automated content moderation with human review queue.
149
+
150
+
## User Activity Feed
151
+
152
+
Build a personalized activity feed.
153
+
154
+
```php
155
+
<?php
156
+
157
+
namespace App\Signals;
158
+
159
+
use App\Models\Activity;
160
+
use App\Models\User;
161
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
162
+
use SocialDept\AtpSignals\Events\SignalEvent;
163
+
use SocialDept\AtpSignals\Signals\Signal;
164
+
165
+
class ActivityFeedSignal extends Signal
166
+
{
167
+
public function eventTypes(): array
168
+
{
169
+
return ['commit'];
170
+
}
171
+
172
+
public function collections(): ?array
173
+
{
174
+
return [
175
+
'app.bsky.feed.post',
176
+
'app.bsky.feed.like',
177
+
'app.bsky.feed.repost',
178
+
];
179
+
}
180
+
181
+
public function operations(): ?array
182
+
{
183
+
return [SignalCommitOperation::Create];
184
+
}
185
+
186
+
public function shouldQueue(): bool
187
+
{
188
+
return true;
189
+
}
190
+
191
+
public function handle(SignalEvent $event): void
192
+
{
193
+
// Check if we're tracking this user
194
+
$user = User::where('did', $event->did)->first();
195
+
196
+
if (!$user) {
197
+
return;
198
+
}
199
+
200
+
// Check if any followers want to see this
201
+
$followerIds = $user->followers()->pluck('id');
202
+
203
+
if ($followerIds->isEmpty()) {
204
+
return;
205
+
}
206
+
207
+
$collection = $event->getCollection();
208
+
209
+
// Create activity for each follower's feed
210
+
foreach ($followerIds as $followerId) {
211
+
Activity::create([
212
+
'user_id' => $followerId,
213
+
'actor_did' => $event->did,
214
+
'type' => $this->getActivityType($collection),
215
+
'data' => $event->toArray(),
216
+
'created_at' => $event->getTimestamp(),
217
+
]);
218
+
}
219
+
}
220
+
221
+
private function getActivityType(string $collection): string
222
+
{
223
+
return match ($collection) {
224
+
'app.bsky.feed.post' => 'post',
225
+
'app.bsky.feed.like' => 'like',
226
+
'app.bsky.feed.repost' => 'repost',
227
+
default => 'unknown',
228
+
};
229
+
}
230
+
}
231
+
```
232
+
233
+
**Use case:** Show users activity from people they follow.
234
+
235
+
## Real-Time Notifications
236
+
237
+
Send notifications for mentions and interactions.
238
+
239
+
```php
240
+
<?php
241
+
242
+
namespace App\Signals;
243
+
244
+
use App\Models\User;
245
+
use App\Notifications\MentionedInPost;
246
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
247
+
use SocialDept\AtpSignals\Events\SignalEvent;
248
+
use SocialDept\AtpSignals\Signals\Signal;
249
+
250
+
class MentionNotificationSignal extends Signal
251
+
{
252
+
public function eventTypes(): array
253
+
{
254
+
return ['commit'];
255
+
}
256
+
257
+
public function collections(): ?array
258
+
{
259
+
return ['app.bsky.feed.post'];
260
+
}
261
+
262
+
public function operations(): ?array
263
+
{
264
+
return [SignalCommitOperation::Create];
265
+
}
266
+
267
+
public function shouldQueue(): bool
268
+
{
269
+
return true;
270
+
}
271
+
272
+
public function handle(SignalEvent $event): void
273
+
{
274
+
$record = $event->getRecord();
275
+
276
+
if (!isset($record->facets)) {
277
+
return;
278
+
}
279
+
280
+
// Extract mentions from facets
281
+
$mentions = collect($record->facets)
282
+
->filter(fn($facet) => isset($facet->features))
283
+
->flatMap(fn($facet) => $facet->features)
284
+
->filter(fn($feature) => $feature->{'$type'} === 'app.bsky.richtext.facet#mention')
285
+
->pluck('did')
286
+
->unique();
287
+
288
+
foreach ($mentions as $mentionedDid) {
289
+
$user = User::where('did', $mentionedDid)->first();
290
+
291
+
if ($user) {
292
+
$user->notify(new MentionedInPost(
293
+
authorDid: $event->did,
294
+
text: $record->text ?? '',
295
+
uri: "at://{$event->did}/app.bsky.feed.post/{$event->commit->rkey}"
296
+
));
297
+
}
298
+
}
299
+
}
300
+
}
301
+
```
302
+
303
+
**Use case:** Real-time notifications when users are mentioned.
304
+
305
+
## Follow Tracker
306
+
307
+
Track follow relationships and send notifications.
308
+
309
+
```php
310
+
<?php
311
+
312
+
namespace App\Signals;
313
+
314
+
use App\Models\Follow;
315
+
use App\Models\User;
316
+
use App\Notifications\NewFollower;
317
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
318
+
use SocialDept\AtpSignals\Events\SignalEvent;
319
+
use SocialDept\AtpSignals\Signals\Signal;
320
+
321
+
class FollowTrackerSignal extends Signal
322
+
{
323
+
public function eventTypes(): array
324
+
{
325
+
return ['commit'];
326
+
}
327
+
328
+
public function collections(): ?array
329
+
{
330
+
return ['app.bsky.graph.follow'];
331
+
}
332
+
333
+
public function operations(): ?array
334
+
{
335
+
return [
336
+
SignalCommitOperation::Create,
337
+
SignalCommitOperation::Delete,
338
+
];
339
+
}
340
+
341
+
public function shouldQueue(): bool
342
+
{
343
+
return true;
344
+
}
345
+
346
+
public function handle(SignalEvent $event): void
347
+
{
348
+
$record = $event->getRecord();
349
+
$operation = $event->getOperation();
350
+
351
+
if ($operation === SignalCommitOperation::Create) {
352
+
$this->handleNewFollow($event, $record);
353
+
} else {
354
+
$this->handleUnfollow($event);
355
+
}
356
+
}
357
+
358
+
private function handleNewFollow(SignalEvent $event, object $record): void
359
+
{
360
+
Follow::create([
361
+
'follower_did' => $event->did,
362
+
'following_did' => $record->subject,
363
+
'created_at' => $record->createdAt ?? now(),
364
+
]);
365
+
366
+
// Notify the followed user
367
+
$followedUser = User::where('did', $record->subject)->first();
368
+
369
+
if ($followedUser) {
370
+
$followedUser->notify(new NewFollower($event->did));
371
+
}
372
+
}
373
+
374
+
private function handleUnfollow(SignalEvent $event): void
375
+
{
376
+
Follow::where('follower_did', $event->did)
377
+
->where('rkey', $event->commit->rkey)
378
+
->delete();
379
+
}
380
+
}
381
+
```
382
+
383
+
**Use case:** Track follows and notify users of new followers.
384
+
385
+
## Search Indexer
386
+
387
+
Index posts for full-text search.
388
+
389
+
```php
390
+
<?php
391
+
392
+
namespace App\Signals;
393
+
394
+
use App\Models\Post;
395
+
use Laravel\Scout\Searchable;
396
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
397
+
use SocialDept\AtpSignals\Events\SignalEvent;
398
+
use SocialDept\AtpSignals\Signals\Signal;
399
+
400
+
class SearchIndexerSignal extends Signal
401
+
{
402
+
public function eventTypes(): array
403
+
{
404
+
return ['commit'];
405
+
}
406
+
407
+
public function collections(): ?array
408
+
{
409
+
return ['app.bsky.feed.post'];
410
+
}
411
+
412
+
public function shouldQueue(): bool
413
+
{
414
+
return true;
415
+
}
416
+
417
+
public function queue(): string
418
+
{
419
+
return 'indexing';
420
+
}
421
+
422
+
public function handle(SignalEvent $event): void
423
+
{
424
+
$operation = $event->getOperation();
425
+
426
+
match ($operation) {
427
+
SignalCommitOperation::Create,
428
+
SignalCommitOperation::Update => $this->indexPost($event),
429
+
SignalCommitOperation::Delete => $this->deletePost($event),
430
+
};
431
+
}
432
+
433
+
private function indexPost(SignalEvent $event): void
434
+
{
435
+
$record = $event->getRecord();
436
+
437
+
$post = Post::updateOrCreate(
438
+
[
439
+
'did' => $event->did,
440
+
'rkey' => $event->commit->rkey,
441
+
],
442
+
[
443
+
'text' => $record->text ?? '',
444
+
'created_at' => $record->createdAt ?? now(),
445
+
'indexed_at' => now(),
446
+
]
447
+
);
448
+
449
+
// Scout automatically indexes
450
+
$post->searchable();
451
+
}
452
+
453
+
private function deletePost(SignalEvent $event): void
454
+
{
455
+
$post = Post::where('did', $event->did)
456
+
->where('rkey', $event->commit->rkey)
457
+
->first();
458
+
459
+
if ($post) {
460
+
$post->unsearchable();
461
+
$post->delete();
462
+
}
463
+
}
464
+
}
465
+
```
466
+
467
+
**Use case:** Full-text search across all Bluesky posts.
468
+
469
+
## Trend Detection
470
+
471
+
Identify trending topics and hashtags.
472
+
473
+
```php
474
+
<?php
475
+
476
+
namespace App\Signals;
477
+
478
+
use App\Models\TrendingTopic;
479
+
use Illuminate\Support\Facades\Cache;
480
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
481
+
use SocialDept\AtpSignals\Events\SignalEvent;
482
+
use SocialDept\AtpSignals\Signals\Signal;
483
+
484
+
class TrendDetectionSignal extends Signal
485
+
{
486
+
public function eventTypes(): array
487
+
{
488
+
return ['commit'];
489
+
}
490
+
491
+
public function collections(): ?array
492
+
{
493
+
return ['app.bsky.feed.post'];
494
+
}
495
+
496
+
public function operations(): ?array
497
+
{
498
+
return [SignalCommitOperation::Create];
499
+
}
500
+
501
+
public function shouldQueue(): bool
502
+
{
503
+
return true;
504
+
}
505
+
506
+
public function handle(SignalEvent $event): void
507
+
{
508
+
$record = $event->getRecord();
509
+
510
+
if (!isset($record->text)) {
511
+
return;
512
+
}
513
+
514
+
// Extract hashtags
515
+
preg_match_all('/#(\w+)/', $record->text, $matches);
516
+
517
+
foreach ($matches[1] as $hashtag) {
518
+
$this->incrementHashtag($hashtag);
519
+
}
520
+
}
521
+
522
+
private function incrementHashtag(string $hashtag): void
523
+
{
524
+
$key = "trending:hashtag:{$hashtag}";
525
+
526
+
// Increment counter (expires after 1 hour)
527
+
$count = Cache::increment($key, 1);
528
+
529
+
if (!Cache::has($key)) {
530
+
Cache::put($key, 1, now()->addHour());
531
+
}
532
+
533
+
// Update trending topics if threshold reached
534
+
if ($count > 100) {
535
+
TrendingTopic::updateOrCreate(
536
+
['hashtag' => $hashtag],
537
+
['count' => $count, 'updated_at' => now()]
538
+
);
539
+
}
540
+
}
541
+
}
542
+
```
543
+
544
+
**Use case:** Identify trending hashtags and topics in real-time.
545
+
546
+
## Custom AppView
547
+
548
+
Index custom collections for your AppView.
549
+
550
+
```php
551
+
<?php
552
+
553
+
namespace App\Signals;
554
+
555
+
use App\Models\Publication;
556
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
557
+
use SocialDept\AtpSignals\Events\SignalEvent;
558
+
use SocialDept\AtpSignals\Signals\Signal;
559
+
560
+
class PublicationIndexerSignal extends Signal
561
+
{
562
+
public function eventTypes(): array
563
+
{
564
+
return ['commit'];
565
+
}
566
+
567
+
public function collections(): ?array
568
+
{
569
+
return [
570
+
'app.offprint.beta.publication',
571
+
'app.offprint.beta.post',
572
+
];
573
+
}
574
+
575
+
public function shouldQueue(): bool
576
+
{
577
+
return true;
578
+
}
579
+
580
+
public function handle(SignalEvent $event): void
581
+
{
582
+
$collection = $event->getCollection();
583
+
$operation = $event->getOperation();
584
+
585
+
if ($collection === 'app.offprint.beta.publication') {
586
+
$this->handlePublication($event, $operation);
587
+
} else {
588
+
$this->handlePost($event, $operation);
589
+
}
590
+
}
591
+
592
+
private function handlePublication(SignalEvent $event, SignalCommitOperation $operation): void
593
+
{
594
+
if ($operation === SignalCommitOperation::Delete) {
595
+
Publication::where('did', $event->did)
596
+
->where('rkey', $event->commit->rkey)
597
+
->delete();
598
+
return;
599
+
}
600
+
601
+
$record = $event->getRecord();
602
+
603
+
Publication::updateOrCreate(
604
+
[
605
+
'did' => $event->did,
606
+
'rkey' => $event->commit->rkey,
607
+
],
608
+
[
609
+
'title' => $record->title ?? '',
610
+
'description' => $record->description ?? null,
611
+
'created_at' => $record->createdAt ?? now(),
612
+
]
613
+
);
614
+
}
615
+
616
+
private function handlePost(SignalEvent $event, SignalCommitOperation $operation): void
617
+
{
618
+
// Handle custom post records
619
+
}
620
+
}
621
+
```
622
+
623
+
**Use case:** Build AT Protocol AppViews with custom collections.
624
+
625
+
## Rate-Limited API Integration
626
+
627
+
Integrate with external APIs respecting rate limits.
628
+
629
+
```php
630
+
<?php
631
+
632
+
namespace App\Signals;
633
+
634
+
use App\Services\ExternalAPIService;
635
+
use Illuminate\Support\Facades\RateLimiter;
636
+
use SocialDept\AtpSignals\Events\SignalEvent;
637
+
use SocialDept\AtpSignals\Signals\Signal;
638
+
639
+
class APIIntegrationSignal extends Signal
640
+
{
641
+
public function __construct(
642
+
private ExternalAPIService $api
643
+
) {}
644
+
645
+
public function eventTypes(): array
646
+
{
647
+
return ['commit'];
648
+
}
649
+
650
+
public function collections(): ?array
651
+
{
652
+
return ['app.bsky.feed.post'];
653
+
}
654
+
655
+
public function shouldQueue(): bool
656
+
{
657
+
return true;
658
+
}
659
+
660
+
public function handle(SignalEvent $event): void
661
+
{
662
+
$record = $event->getRecord();
663
+
664
+
// Rate limit: 100 calls per minute
665
+
$executed = RateLimiter::attempt(
666
+
'external-api',
667
+
$perMinute = 100,
668
+
function () use ($event, $record) {
669
+
$this->api->sendPost([
670
+
'author' => $event->did,
671
+
'text' => $record->text ?? '',
672
+
'timestamp' => $event->getTimestamp(),
673
+
]);
674
+
}
675
+
);
676
+
677
+
if (!$executed) {
678
+
// Re-queue for later
679
+
dispatch(fn() => $this->handle($event))
680
+
->delay(now()->addMinutes(1));
681
+
}
682
+
}
683
+
}
684
+
```
685
+
686
+
**Use case:** Mirror content to external platforms with rate limiting.
687
+
688
+
## Multi-Collection Analytics
689
+
690
+
Track engagement across multiple collection types.
691
+
692
+
```php
693
+
<?php
694
+
695
+
namespace App\Signals;
696
+
697
+
use App\Models\UserMetrics;
698
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
699
+
use SocialDept\AtpSignals\Events\SignalEvent;
700
+
use SocialDept\AtpSignals\Signals\Signal;
701
+
702
+
class UserMetricsSignal extends Signal
703
+
{
704
+
public function eventTypes(): array
705
+
{
706
+
return ['commit'];
707
+
}
708
+
709
+
public function collections(): ?array
710
+
{
711
+
return ['app.bsky.feed.*', 'app.bsky.graph.*'];
712
+
}
713
+
714
+
public function operations(): ?array
715
+
{
716
+
return [SignalCommitOperation::Create];
717
+
}
718
+
719
+
public function shouldQueue(): bool
720
+
{
721
+
return true;
722
+
}
723
+
724
+
public function handle(SignalEvent $event): void
725
+
{
726
+
$collection = $event->getCollection();
727
+
728
+
$metrics = UserMetrics::firstOrCreate(
729
+
['did' => $event->did],
730
+
['total_posts' => 0, 'total_likes' => 0, 'total_follows' => 0]
731
+
);
732
+
733
+
match ($collection) {
734
+
'app.bsky.feed.post' => $metrics->increment('total_posts'),
735
+
'app.bsky.feed.like' => $metrics->increment('total_likes'),
736
+
'app.bsky.graph.follow' => $metrics->increment('total_follows'),
737
+
default => null,
738
+
};
739
+
740
+
$metrics->touch('last_activity_at');
741
+
}
742
+
}
743
+
```
744
+
745
+
**Use case:** User activity metrics and leaderboards.
746
+
747
+
## Performance Tips
748
+
749
+
### Batch Database Operations
750
+
751
+
```php
752
+
public function handle(SignalEvent $event): void
753
+
{
754
+
// Bad - individual inserts
755
+
Post::create([...]);
756
+
757
+
// Good - batch inserts
758
+
$posts = Cache::get('pending_posts', []);
759
+
$posts[] = [...];
760
+
761
+
if (count($posts) >= 100) {
762
+
Post::insert($posts);
763
+
Cache::forget('pending_posts');
764
+
} else {
765
+
Cache::put('pending_posts', $posts, now()->addMinutes(5));
766
+
}
767
+
}
768
+
```
769
+
770
+
### Use Queues for Heavy Operations
771
+
772
+
```php
773
+
public function shouldQueue(): bool
774
+
{
775
+
// Queue if operation takes > 100ms
776
+
return true;
777
+
}
778
+
```
779
+
780
+
### Add Indexes for Filtering
781
+
782
+
```php
783
+
// Migration for fast lookups
784
+
Schema::table('posts', function (Blueprint $table) {
785
+
$table->index(['did', 'rkey']);
786
+
$table->index('created_at');
787
+
});
788
+
```
789
+
790
+
## Next Steps
791
+
792
+
- **[Review signal architecture โ](signals.md)** - Understand Signal structure
793
+
- **[Learn about filtering โ](filtering.md)** - Master event filtering
794
+
- **[Explore queue integration โ](queues.md)** - Build high-performance Signals
795
+
- **[Configure your setup โ](configuration.md)** - Optimize configuration
+704
docs/filtering.md
+704
docs/filtering.md
···
1
+
# Filtering Events
2
+
3
+
Filtering is how you control which events your Signals process. Signal provides multiple layers of filtering to help you target exactly the events you care about.
4
+
5
+
## Why Filter?
6
+
7
+
The AT Protocol generates millions of events per hour. Without filtering:
8
+
9
+
- Your Signals would process every event (slow and expensive)
10
+
- Your database would fill with irrelevant data
11
+
- Your queues would be overwhelmed
12
+
- Your costs would skyrocket
13
+
14
+
Filtering lets you focus on what matters.
15
+
16
+
## Filter Layers
17
+
18
+
Signal provides four filtering layers, applied in order:
19
+
20
+
1. **Event Type Filtering** - Which kind of events (commit, identity, account)
21
+
2. **Collection Filtering** - Which AT Protocol collections
22
+
3. **Operation Filtering** - Which operations (create, update, delete)
23
+
4. **DID Filtering** - Which users
24
+
5. **Custom Filtering** - Your own logic
25
+
26
+
## Event Type Filtering
27
+
28
+
The most basic filter - required for all Signals.
29
+
30
+
### Available Event Types
31
+
32
+
```php
33
+
use SocialDept\AtpSignals\Enums\SignalEventType;
34
+
35
+
public function eventTypes(): array
36
+
{
37
+
return [SignalEventType::Commit]; // Most common
38
+
// Or: return ['commit'];
39
+
}
40
+
```
41
+
42
+
**Three event types:**
43
+
44
+
| Type | Description | Use Cases |
45
+
|------------|--------------------|----------------------------------------|
46
+
| `commit` | Repository changes | Posts, likes, follows, profile updates |
47
+
| `identity` | Handle changes | Username updates, account migrations |
48
+
| `account` | Account status | Deactivation, suspension |
49
+
50
+
### Multiple Event Types
51
+
52
+
Listen to multiple types in one Signal:
53
+
54
+
```php
55
+
public function eventTypes(): array
56
+
{
57
+
return [
58
+
SignalEventType::Commit,
59
+
SignalEventType::Identity,
60
+
];
61
+
}
62
+
```
63
+
64
+
Then check the type in your handler:
65
+
66
+
```php
67
+
public function handle(SignalEvent $event): void
68
+
{
69
+
if ($event->isCommit()) {
70
+
$this->handleCommit($event);
71
+
}
72
+
73
+
if ($event->isIdentity()) {
74
+
$this->handleIdentity($event);
75
+
}
76
+
}
77
+
```
78
+
79
+
## Collection Filtering
80
+
81
+
Collections represent different types of data in the AT Protocol.
82
+
83
+
### Basic Collection Filter
84
+
85
+
```php
86
+
public function collections(): ?array
87
+
{
88
+
return ['app.bsky.feed.post'];
89
+
}
90
+
```
91
+
92
+
### No Filter (All Collections)
93
+
94
+
Return `null` to process all collections:
95
+
96
+
```php
97
+
public function collections(): ?array
98
+
{
99
+
return null; // Handle everything
100
+
}
101
+
```
102
+
103
+
### Multiple Collections
104
+
105
+
```php
106
+
public function collections(): ?array
107
+
{
108
+
return [
109
+
'app.bsky.feed.post',
110
+
'app.bsky.feed.like',
111
+
'app.bsky.feed.repost',
112
+
];
113
+
}
114
+
```
115
+
116
+
### Wildcard Patterns
117
+
118
+
Use `*` to match multiple collections:
119
+
120
+
```php
121
+
public function collections(): ?array
122
+
{
123
+
return ['app.bsky.feed.*'];
124
+
}
125
+
```
126
+
127
+
**This matches:**
128
+
- `app.bsky.feed.post`
129
+
- `app.bsky.feed.like`
130
+
- `app.bsky.feed.repost`
131
+
- Any other `app.bsky.feed.*` collection
132
+
133
+
### Common Collection Patterns
134
+
135
+
| Pattern | Matches | Use Case |
136
+
|--------------------|-------------------------|------------------------|
137
+
| `app.bsky.feed.*` | All feed interactions | Posts, likes, reposts |
138
+
| `app.bsky.graph.*` | All social graph | Follows, blocks, mutes |
139
+
| `app.bsky.actor.*` | All profile changes | Profile updates |
140
+
| `app.bsky.*` | All Bluesky collections | Everything Bluesky |
141
+
| `app.yourapp.*` | Your custom collections | Custom AppView |
142
+
143
+
### Mixing Exact and Wildcards
144
+
145
+
Combine exact matches with wildcards:
146
+
147
+
```php
148
+
public function collections(): ?array
149
+
{
150
+
return [
151
+
'app.bsky.feed.post', // Exact: only posts
152
+
'app.bsky.graph.*', // Wildcard: all graph events
153
+
'app.myapp.custom.record', // Exact: custom collection
154
+
];
155
+
}
156
+
```
157
+
158
+
### Standard Bluesky Collections
159
+
160
+
**Feed Collections** (`app.bsky.feed.*`):
161
+
- `app.bsky.feed.post` - Posts (text, images, videos)
162
+
- `app.bsky.feed.like` - Likes on posts
163
+
- `app.bsky.feed.repost` - Reposts (shares)
164
+
- `app.bsky.feed.threadgate` - Thread reply controls
165
+
- `app.bsky.feed.generator` - Custom feed generators
166
+
167
+
**Graph Collections** (`app.bsky.graph.*`):
168
+
- `app.bsky.graph.follow` - Follow relationships
169
+
- `app.bsky.graph.block` - Blocked users
170
+
- `app.bsky.graph.list` - User lists
171
+
- `app.bsky.graph.listitem` - List memberships
172
+
- `app.bsky.graph.listblock` - List blocks
173
+
174
+
**Actor Collections** (`app.bsky.actor.*`):
175
+
- `app.bsky.actor.profile` - User profiles
176
+
177
+
**Labeler Collections** (`app.bsky.labeler.*`):
178
+
- `app.bsky.labeler.service` - Labeler services
179
+
180
+
### Important: Jetstream vs Firehose Filtering
181
+
182
+
**Jetstream Mode:**
183
+
- Exact collection names are sent to server for filtering (efficient)
184
+
- Wildcards work client-side only (you receive more data)
185
+
186
+
**Firehose Mode:**
187
+
- All filtering is client-side
188
+
- Wildcards work normally (no difference in data received)
189
+
190
+
[Learn more about modes โ](modes.md)
191
+
192
+
### Custom Collections (AppViews)
193
+
194
+
Filter your own custom collections:
195
+
196
+
```php
197
+
public function collections(): ?array
198
+
{
199
+
return [
200
+
'app.offprint.beta.publication',
201
+
'app.offprint.beta.post',
202
+
];
203
+
}
204
+
```
205
+
206
+
## Operation Filtering
207
+
208
+
Filter by operation type (only applies to commit events).
209
+
210
+
### Available Operations
211
+
212
+
```php
213
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
214
+
215
+
public function operations(): ?array
216
+
{
217
+
return [SignalCommitOperation::Create];
218
+
// Or: return ['create'];
219
+
}
220
+
```
221
+
222
+
**Three operation types:**
223
+
224
+
| Operation | Description | Example |
225
+
|-----------|------------------|-----------------|
226
+
| `create` | New records | Creating a post |
227
+
| `update` | Modified records | Editing a post |
228
+
| `delete` | Removed records | Deleting a post |
229
+
230
+
### No Filter (All Operations)
231
+
232
+
```php
233
+
public function operations(): ?array
234
+
{
235
+
return null; // Handle all operations
236
+
}
237
+
```
238
+
239
+
### Multiple Operations
240
+
241
+
```php
242
+
public function operations(): ?array
243
+
{
244
+
return [
245
+
SignalCommitOperation::Create,
246
+
SignalCommitOperation::Update,
247
+
];
248
+
// Or: return ['create', 'update'];
249
+
}
250
+
```
251
+
252
+
### Common Patterns
253
+
254
+
**Only track new content:**
255
+
```php
256
+
public function operations(): ?array
257
+
{
258
+
return [SignalCommitOperation::Create];
259
+
}
260
+
```
261
+
262
+
**Track modifications:**
263
+
```php
264
+
public function operations(): ?array
265
+
{
266
+
return [SignalCommitOperation::Update];
267
+
}
268
+
```
269
+
270
+
**Cleanup on deletions:**
271
+
```php
272
+
public function operations(): ?array
273
+
{
274
+
return [SignalCommitOperation::Delete];
275
+
}
276
+
```
277
+
278
+
### Checking Operations in Handler
279
+
280
+
You can also check operation type in your handler:
281
+
282
+
```php
283
+
public function handle(SignalEvent $event): void
284
+
{
285
+
$operation = $event->getOperation();
286
+
287
+
// Using enum
288
+
if ($operation === SignalCommitOperation::Create) {
289
+
$this->createRecord($event);
290
+
}
291
+
292
+
// Using commit helper
293
+
if ($event->commit->isCreate()) {
294
+
$this->createRecord($event);
295
+
}
296
+
297
+
if ($event->commit->isUpdate()) {
298
+
$this->updateRecord($event);
299
+
}
300
+
301
+
if ($event->commit->isDelete()) {
302
+
$this->deleteRecord($event);
303
+
}
304
+
}
305
+
```
306
+
307
+
## DID Filtering
308
+
309
+
Filter events by specific users (DIDs).
310
+
311
+
### Basic DID Filter
312
+
313
+
```php
314
+
public function dids(): ?array
315
+
{
316
+
return [
317
+
'did:plc:z72i7hdynmk6r22z27h6tvur',
318
+
];
319
+
}
320
+
```
321
+
322
+
### No Filter (All Users)
323
+
324
+
```php
325
+
public function dids(): ?array
326
+
{
327
+
return null; // Handle all users
328
+
}
329
+
```
330
+
331
+
### Multiple DIDs
332
+
333
+
```php
334
+
public function dids(): ?array
335
+
{
336
+
return [
337
+
'did:plc:z72i7hdynmk6r22z27h6tvur',
338
+
'did:plc:ragtjsm2j2vknwkz3zp4oxrd',
339
+
];
340
+
}
341
+
```
342
+
343
+
### Use Cases
344
+
345
+
**Monitor specific accounts:**
346
+
```php
347
+
// Track posts from specific content creators
348
+
public function collections(): ?array
349
+
{
350
+
return ['app.bsky.feed.post'];
351
+
}
352
+
353
+
public function dids(): ?array
354
+
{
355
+
return [
356
+
'did:plc:z72i7hdynmk6r22z27h6tvur', // Creator 1
357
+
'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // Creator 2
358
+
];
359
+
}
360
+
```
361
+
362
+
**Dynamic DID filtering:**
363
+
```php
364
+
use App\Models\MonitoredAccount;
365
+
366
+
public function dids(): ?array
367
+
{
368
+
return MonitoredAccount::pluck('did')->toArray();
369
+
}
370
+
```
371
+
372
+
## Custom Filtering
373
+
374
+
Implement complex filtering logic with `shouldHandle()`.
375
+
376
+
### Basic Custom Filter
377
+
378
+
```php
379
+
public function shouldHandle(SignalEvent $event): bool
380
+
{
381
+
// Only handle posts with images
382
+
if ($event->isCommit() && $event->getCollection() === 'app.bsky.feed.post') {
383
+
$record = $event->getRecord();
384
+
return isset($record->embed);
385
+
}
386
+
387
+
return true;
388
+
}
389
+
```
390
+
391
+
### Advanced Examples
392
+
393
+
**Filter by text content:**
394
+
```php
395
+
public function shouldHandle(SignalEvent $event): bool
396
+
{
397
+
$record = $event->getRecord();
398
+
399
+
if (!isset($record->text)) {
400
+
return false;
401
+
}
402
+
403
+
// Only handle posts mentioning "Laravel"
404
+
return str_contains($record->text, 'Laravel');
405
+
}
406
+
```
407
+
408
+
**Filter by language:**
409
+
```php
410
+
public function shouldHandle(SignalEvent $event): bool
411
+
{
412
+
$record = $event->getRecord();
413
+
414
+
// Only handle English posts
415
+
return ($record->langs[0] ?? null) === 'en';
416
+
}
417
+
```
418
+
419
+
**Filter by engagement:**
420
+
```php
421
+
use App\Services\EngagementCalculator;
422
+
423
+
public function shouldHandle(SignalEvent $event): bool
424
+
{
425
+
$engagement = EngagementCalculator::calculate($event);
426
+
427
+
// Only handle high-engagement content
428
+
return $engagement > 100;
429
+
}
430
+
```
431
+
432
+
**Time-based filtering:**
433
+
```php
434
+
public function shouldHandle(SignalEvent $event): bool
435
+
{
436
+
$timestamp = $event->getTimestamp();
437
+
438
+
// Only handle events from the last hour
439
+
return $timestamp->isAfter(now()->subHour());
440
+
}
441
+
```
442
+
443
+
## Combining Filters
444
+
445
+
Stack multiple filter layers for precise targeting:
446
+
447
+
```php
448
+
class HighEngagementPostsSignal extends Signal
449
+
{
450
+
// Layer 1: Event type
451
+
public function eventTypes(): array
452
+
{
453
+
return ['commit'];
454
+
}
455
+
456
+
// Layer 2: Collection
457
+
public function collections(): ?array
458
+
{
459
+
return ['app.bsky.feed.post'];
460
+
}
461
+
462
+
// Layer 3: Operation
463
+
public function operations(): ?array
464
+
{
465
+
return [SignalCommitOperation::Create];
466
+
}
467
+
468
+
// Layer 4: Custom logic
469
+
public function shouldHandle(SignalEvent $event): bool
470
+
{
471
+
$record = $event->getRecord();
472
+
473
+
// Must have text
474
+
if (!isset($record->text)) {
475
+
return false;
476
+
}
477
+
478
+
// Must be longer than 100 characters
479
+
if (strlen($record->text) < 100) {
480
+
return false;
481
+
}
482
+
483
+
// Must have media
484
+
if (!isset($record->embed)) {
485
+
return false;
486
+
}
487
+
488
+
return true;
489
+
}
490
+
491
+
public function handle(SignalEvent $event): void
492
+
{
493
+
// Only high-quality posts make it here
494
+
}
495
+
}
496
+
```
497
+
498
+
## Performance Considerations
499
+
500
+
### Server-Side vs Client-Side Filtering
501
+
502
+
**Jetstream Mode (Server-Side):**
503
+
- Collections filter applied on server (efficient)
504
+
- Only receives matching events
505
+
- Lower bandwidth usage
506
+
507
+
```php
508
+
// These collections are sent to Jetstream server
509
+
public function collections(): ?array
510
+
{
511
+
return ['app.bsky.feed.post', 'app.bsky.feed.like'];
512
+
}
513
+
```
514
+
515
+
**Firehose Mode (Client-Side):**
516
+
- All filtering happens in your application
517
+
- Receives all events (higher bandwidth)
518
+
- More control but higher cost
519
+
520
+
[Learn more about modes โ](modes.md)
521
+
522
+
### Filter Early
523
+
524
+
Apply the most restrictive filters first:
525
+
526
+
```php
527
+
// Good - filters early
528
+
public function eventTypes(): array
529
+
{
530
+
return ['commit']; // Narrows to commits only
531
+
}
532
+
533
+
public function collections(): ?array
534
+
{
535
+
return ['app.bsky.feed.post']; // Further narrows to posts
536
+
}
537
+
538
+
// Less ideal - too broad
539
+
public function eventTypes(): array
540
+
{
541
+
return ['commit', 'identity', 'account']; // Too many events
542
+
}
543
+
544
+
public function shouldHandle(SignalEvent $event): bool
545
+
{
546
+
// Filtering everything in custom logic (expensive)
547
+
return $event->isCommit() && $event->getCollection() === 'app.bsky.feed.post';
548
+
}
549
+
```
550
+
551
+
### Avoid Heavy Logic in shouldHandle()
552
+
553
+
Keep custom filtering lightweight:
554
+
555
+
```php
556
+
// Good - lightweight checks
557
+
public function shouldHandle(SignalEvent $event): bool
558
+
{
559
+
$record = $event->getRecord();
560
+
return isset($record->text) && strlen($record->text) > 10;
561
+
}
562
+
563
+
// Less ideal - heavy database queries
564
+
public function shouldHandle(SignalEvent $event): bool
565
+
{
566
+
// Database query on every event (slow!)
567
+
return User::where('did', $event->did)->exists();
568
+
}
569
+
```
570
+
571
+
If you need heavy logic, use queues:
572
+
573
+
```php
574
+
public function shouldQueue(): bool
575
+
{
576
+
return true; // Move heavy work to queue
577
+
}
578
+
```
579
+
580
+
## Common Filter Patterns
581
+
582
+
### Track All Activity from Specific Users
583
+
584
+
```php
585
+
public function eventTypes(): array
586
+
{
587
+
return ['commit'];
588
+
}
589
+
590
+
public function dids(): ?array
591
+
{
592
+
return [
593
+
'did:plc:z72i7hdynmk6r22z27h6tvur',
594
+
];
595
+
}
596
+
```
597
+
598
+
### Monitor All Feed Activity
599
+
600
+
```php
601
+
public function eventTypes(): array
602
+
{
603
+
return ['commit'];
604
+
}
605
+
606
+
public function collections(): ?array
607
+
{
608
+
return ['app.bsky.feed.*'];
609
+
}
610
+
```
611
+
612
+
### Track Only New Posts
613
+
614
+
```php
615
+
public function eventTypes(): array
616
+
{
617
+
return ['commit'];
618
+
}
619
+
620
+
public function collections(): ?array
621
+
{
622
+
return ['app.bsky.feed.post'];
623
+
}
624
+
625
+
public function operations(): ?array
626
+
{
627
+
return [SignalCommitOperation::Create];
628
+
}
629
+
```
630
+
631
+
### Monitor Content Deletions
632
+
633
+
```php
634
+
public function eventTypes(): array
635
+
{
636
+
return ['commit'];
637
+
}
638
+
639
+
public function operations(): ?array
640
+
{
641
+
return [SignalCommitOperation::Delete];
642
+
}
643
+
```
644
+
645
+
### Track Profile Changes
646
+
647
+
```php
648
+
public function eventTypes(): array
649
+
{
650
+
return ['commit'];
651
+
}
652
+
653
+
public function collections(): ?array
654
+
{
655
+
return ['app.bsky.actor.profile'];
656
+
}
657
+
```
658
+
659
+
### Monitor Handle Changes
660
+
661
+
```php
662
+
public function eventTypes(): array
663
+
{
664
+
return ['identity'];
665
+
}
666
+
```
667
+
668
+
## Debugging Filters
669
+
670
+
### Log What's Being Filtered
671
+
672
+
```php
673
+
public function shouldHandle(SignalEvent $event): bool
674
+
{
675
+
$shouldHandle = $this->myCustomLogic($event);
676
+
677
+
if (!$shouldHandle) {
678
+
Log::debug('Event filtered out', [
679
+
'signal' => static::class,
680
+
'did' => $event->did,
681
+
'collection' => $event->getCollection(),
682
+
'reason' => 'Failed custom logic',
683
+
]);
684
+
}
685
+
686
+
return $shouldHandle;
687
+
}
688
+
```
689
+
690
+
### Test Your Filters
691
+
692
+
```bash
693
+
php artisan signal:test YourSignal
694
+
```
695
+
696
+
This runs your Signal with sample data to verify filtering works correctly.
697
+
698
+
[Learn more about testing โ](testing.md)
699
+
700
+
## Next Steps
701
+
702
+
- **[Understand Jetstream vs Firehose โ](modes.md)** - Choose the right mode for your filters
703
+
- **[Learn about queue integration โ](queues.md)** - Handle high-volume filtered events
704
+
- **[See real-world examples โ](examples.md)** - Learn from production filter patterns
+188
docs/installation.md
+188
docs/installation.md
···
1
+
# Installation
2
+
3
+
Signal is designed to be installed quickly and easily in any Laravel 11+ application.
4
+
5
+
## Requirements
6
+
7
+
Before installing Signal, ensure your environment meets these requirements:
8
+
9
+
- **PHP 8.2 or higher**
10
+
- **Laravel 11.0 or higher**
11
+
- **WebSocket support** (enabled by default in most environments)
12
+
- **Database** (for cursor storage)
13
+
14
+
## Composer Installation
15
+
16
+
Install Signal via Composer:
17
+
18
+
```bash
19
+
composer require socialdept/atp-signals
20
+
```
21
+
22
+
## Quick Setup
23
+
24
+
Run the installation command to set up everything automatically:
25
+
26
+
```bash
27
+
php artisan signal:install
28
+
```
29
+
30
+
This interactive command will:
31
+
32
+
1. Publish the configuration file to `config/signal.php`
33
+
2. Publish database migrations for cursor storage
34
+
3. Ask if you'd like to run migrations immediately
35
+
4. Display next steps and helpful information
36
+
37
+
### What Gets Created
38
+
39
+
After installation, you'll have:
40
+
41
+
- **Configuration file**: `config/signal.php` - All Signal settings
42
+
- **Migration**: `database/migrations/2024_01_01_000000_create_signal_cursors_table.php` - Cursor storage
43
+
- **Signal directory**: `app/Signals/` - Where your Signals live (created when you make your first Signal)
44
+
45
+
## Manual Installation
46
+
47
+
If you prefer more control, you can install manually:
48
+
49
+
### 1. Publish Configuration
50
+
51
+
```bash
52
+
php artisan vendor:publish --tag=signal-config
53
+
```
54
+
55
+
This creates `config/signal.php` with all available options.
56
+
57
+
### 2. Publish Migrations
58
+
59
+
```bash
60
+
php artisan vendor:publish --tag=signal-migrations
61
+
```
62
+
63
+
This creates the cursor storage migration in `database/migrations/`.
64
+
65
+
### 3. Run Migrations
66
+
67
+
```bash
68
+
php artisan migrate
69
+
```
70
+
71
+
This creates the `signal_cursors` table for resuming from last position after disconnections.
72
+
73
+
## Environment Configuration
74
+
75
+
Add Signal configuration to your `.env` file:
76
+
77
+
```env
78
+
# Consumer Mode (jetstream or firehose)
79
+
SIGNAL_MODE=jetstream
80
+
81
+
# Jetstream URL (if using jetstream mode)
82
+
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
83
+
84
+
# Firehose Host (if using firehose mode)
85
+
SIGNAL_FIREHOSE_HOST=bsky.network
86
+
87
+
# Optional: Cursor Storage Driver (database, redis, or file)
88
+
SIGNAL_CURSOR_STORAGE=database
89
+
90
+
# Optional: Queue Configuration
91
+
SIGNAL_QUEUE_CONNECTION=redis
92
+
SIGNAL_QUEUE=signal
93
+
```
94
+
95
+
## Choosing Your Mode
96
+
97
+
Signal supports two modes for consuming events. Choose based on your use case:
98
+
99
+
### Jetstream Mode (Recommended)
100
+
101
+
Best for most applications:
102
+
103
+
```env
104
+
SIGNAL_MODE=jetstream
105
+
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
106
+
```
107
+
108
+
**Advantages:**
109
+
- Simplified JSON events (easy to work with)
110
+
- Server-side collection filtering (efficient)
111
+
- Lower bandwidth and processing overhead
112
+
113
+
### Firehose Mode
114
+
115
+
Best for comprehensive indexing and raw data access:
116
+
117
+
```env
118
+
SIGNAL_MODE=firehose
119
+
SIGNAL_FIREHOSE_HOST=bsky.network
120
+
```
121
+
122
+
**Advantages:**
123
+
- Access to raw CBOR/CAR data
124
+
- Full AT Protocol event stream
125
+
- Complete control over event processing
126
+
127
+
**Trade-offs:**
128
+
- Client-side filtering only (higher bandwidth)
129
+
- More processing overhead
130
+
131
+
[Learn more about choosing the right mode โ](modes.md)
132
+
133
+
## Verify Installation
134
+
135
+
Check that Signal is installed correctly:
136
+
137
+
```bash
138
+
php artisan signal:list
139
+
```
140
+
141
+
This should display available Signals (initially none until you create them).
142
+
143
+
## Next Steps
144
+
145
+
Now that Signal is installed, you're ready to start building:
146
+
147
+
1. **[Create your first Signal โ](quickstart.md)**
148
+
2. **[Learn about Signal architecture โ](signals.md)**
149
+
3. **[Understand filtering options โ](filtering.md)**
150
+
151
+
## Troubleshooting
152
+
153
+
### Migration Already Exists
154
+
155
+
If you see "migration already exists" when running `signal:install`, you've likely already installed Signal. You can safely skip this step.
156
+
157
+
### WebSocket Connection Issues
158
+
159
+
If you experience WebSocket connection issues:
160
+
161
+
1. Verify your firewall allows WebSocket connections
162
+
2. Check that your hosting environment supports WebSockets
163
+
3. Try switching Jetstream endpoints (US East vs US West)
164
+
165
+
### Permission Errors
166
+
167
+
If you encounter permission errors with cursor storage:
168
+
169
+
- **Database mode**: Ensure database connection is configured correctly
170
+
- **Redis mode**: Verify Redis connection is available
171
+
- **File mode**: Check that Laravel has write permissions to `storage/app/signal/`
172
+
173
+
## Uninstallation
174
+
175
+
To remove Signal from your application:
176
+
177
+
```bash
178
+
# Remove the package
179
+
composer remove socialdept/atp-signals
180
+
181
+
# Optionally, rollback migrations
182
+
php artisan migrate:rollback
183
+
```
184
+
185
+
You can also manually delete:
186
+
- `config/signal.php`
187
+
- `app/Signals/` directory
188
+
- Signal-related migrations
+493
docs/modes.md
+493
docs/modes.md
···
1
+
# Jetstream vs Firehose
2
+
3
+
Signal supports two modes for consuming AT Protocol events. Understanding the differences is crucial for building efficient, scalable applications.
4
+
5
+
## Quick Comparison
6
+
7
+
| Feature | Jetstream | Firehose |
8
+
|------------------|-------------------|------------------------|
9
+
| **Event Format** | Simplified JSON | Raw CBOR/CAR |
10
+
| **Filtering** | Server-side | Client-side |
11
+
| **Bandwidth** | Lower | Higher |
12
+
| **Processing** | Lighter | Heavier |
13
+
| **Best For** | Most applications | Comprehensive indexing |
14
+
15
+
## Jetstream Mode
16
+
17
+
Jetstream is a **simplified, JSON-based event stream** built on top of the AT Protocol Firehose.
18
+
19
+
### When to Use Jetstream
20
+
21
+
Choose Jetstream if you're:
22
+
23
+
- Building production applications where efficiency matters
24
+
- Concerned about bandwidth and server costs
25
+
- Processing high volumes of events
26
+
- Want server-side filtering for reduced bandwidth
27
+
28
+
### Configuration
29
+
30
+
Set Jetstream as your mode in `.env`:
31
+
32
+
```env
33
+
SIGNAL_MODE=jetstream
34
+
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
35
+
```
36
+
37
+
### Available Endpoints
38
+
39
+
Jetstream has multiple regional endpoints:
40
+
41
+
**US East (Default):**
42
+
```env
43
+
SIGNAL_JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
44
+
```
45
+
46
+
**US West:**
47
+
```env
48
+
SIGNAL_JETSTREAM_URL=wss://jetstream1.us-west.bsky.network
49
+
```
50
+
51
+
Choose the endpoint closest to your server for best performance.
52
+
53
+
### Advantages
54
+
55
+
**1. Simplified JSON Format**
56
+
57
+
Events arrive as clean JSON objects:
58
+
59
+
```json
60
+
{
61
+
"did": "did:plc:z72i7hdynmk6r22z27h6tvur",
62
+
"time_us": 1234567890,
63
+
"kind": "commit",
64
+
"commit": {
65
+
"rev": "abc123",
66
+
"operation": "create",
67
+
"collection": "app.bsky.feed.post",
68
+
"rkey": "3k2yihcrr2c2a",
69
+
"record": {
70
+
"text": "Hello World!",
71
+
"createdAt": "2024-01-15T10:30:00Z"
72
+
}
73
+
}
74
+
}
75
+
```
76
+
77
+
No complex parsing or decoding required.
78
+
79
+
**2. Server-Side Filtering**
80
+
81
+
Your collection filters are sent to Jetstream:
82
+
83
+
```php
84
+
public function collections(): ?array
85
+
{
86
+
return ['app.bsky.feed.post', 'app.bsky.feed.like'];
87
+
}
88
+
```
89
+
90
+
Jetstream only sends matching events, dramatically reducing bandwidth.
91
+
92
+
**3. Lower Bandwidth**
93
+
94
+
Only receive the events you care about:
95
+
96
+
- **Jetstream**: Receive ~1,000 events/sec for specific collections
97
+
- **Firehose**: Receive ~50,000 events/sec for everything
98
+
99
+
**4. Lower Processing Overhead**
100
+
101
+
JSON parsing is faster than CBOR/CAR decoding:
102
+
103
+
- **Jetstream**: Simple JSON deserialization
104
+
- **Firehose**: Complex CBOR/CAR decoding with `revolution/laravel-bluesky`
105
+
106
+
### Limitations
107
+
108
+
**1. Client-Side Wildcards**
109
+
110
+
Wildcard patterns work client-side only:
111
+
112
+
```php
113
+
public function collections(): ?array
114
+
{
115
+
return ['app.bsky.feed.*']; // Still receives all collections
116
+
}
117
+
```
118
+
119
+
The wildcard matching happens in your app, not on the server.
120
+
121
+
### Example Configuration
122
+
123
+
```php
124
+
// config/signal.php
125
+
return [
126
+
'mode' => env('SIGNAL_MODE', 'jetstream'),
127
+
128
+
'jetstream' => [
129
+
'websocket_url' => env(
130
+
'SIGNAL_JETSTREAM_URL',
131
+
'wss://jetstream2.us-east.bsky.network'
132
+
),
133
+
],
134
+
];
135
+
```
136
+
137
+
## Firehose Mode
138
+
139
+
Firehose is the **raw AT Protocol event stream** with comprehensive support for all collections.
140
+
141
+
### When to Use Firehose
142
+
143
+
Choose Firehose if you're:
144
+
145
+
- Building comprehensive indexing systems
146
+
- Developing AT Protocol infrastructure
147
+
- Need access to raw CBOR/CAR data
148
+
- Prefer client-side filtering control
149
+
150
+
### Configuration
151
+
152
+
Set Firehose as your mode in `.env`:
153
+
154
+
```env
155
+
SIGNAL_MODE=firehose
156
+
SIGNAL_FIREHOSE_HOST=bsky.network
157
+
```
158
+
159
+
### Advantages
160
+
161
+
**1. Raw Event Access**
162
+
163
+
Full access to raw AT Protocol data:
164
+
165
+
```php
166
+
public function handle(SignalEvent $event): void
167
+
{
168
+
// Access raw CBOR/CAR data
169
+
$cid = $event->commit->cid;
170
+
$blocks = $event->commit->blocks;
171
+
}
172
+
```
173
+
174
+
**2. Comprehensive Events**
175
+
176
+
Every event from the AT Protocol network arrives:
177
+
178
+
- All collections (standard and custom)
179
+
- All operations (create, update, delete)
180
+
- All metadata and context
181
+
- Complete repository commits
182
+
183
+
**3. Complete Control**
184
+
185
+
Full access to raw AT Protocol data:
186
+
187
+
- CID (Content Identifiers)
188
+
- Block structures
189
+
- CAR file data
190
+
- Complete repository commits
191
+
192
+
### Trade-offs
193
+
194
+
**1. Client-Side Filtering**
195
+
196
+
All filtering happens in your application:
197
+
198
+
```php
199
+
public function collections(): ?array
200
+
{
201
+
return ['app.bsky.feed.post']; // Still receives all events
202
+
}
203
+
```
204
+
205
+
Your app receives everything and filters locally.
206
+
207
+
**2. Higher Bandwidth**
208
+
209
+
Receive the full event stream:
210
+
211
+
- **~50,000+ events per second** during peak times
212
+
- **~10-50 MB/s** of data throughput
213
+
- Requires adequate network capacity
214
+
215
+
**3. More Processing Overhead**
216
+
217
+
Complex CBOR/CAR decoding:
218
+
219
+
```php
220
+
// Signal automatically handles decoding using revolution/laravel-bluesky
221
+
$record = $event->getRecord(); // Decoded from CBOR/CAR
222
+
```
223
+
224
+
Processing is more CPU-intensive than Jetstream's JSON.
225
+
226
+
**4. Requires revolution/laravel-bluesky**
227
+
228
+
Firehose mode depends on the `revolution/laravel-bluesky` package for decoding:
229
+
230
+
```bash
231
+
composer require revolution/bluesky
232
+
```
233
+
234
+
Signal handles this dependency automatically.
235
+
236
+
### Example Configuration
237
+
238
+
```php
239
+
// config/signal.php
240
+
return [
241
+
'mode' => env('SIGNAL_MODE', 'jetstream'),
242
+
243
+
'firehose' => [
244
+
'host' => env('SIGNAL_FIREHOSE_HOST', 'bsky.network'),
245
+
],
246
+
];
247
+
```
248
+
249
+
The WebSocket URL is constructed as:
250
+
```
251
+
wss://{host}/xrpc/com.atproto.sync.subscribeRepos
252
+
```
253
+
254
+
## Choosing the Right Mode
255
+
256
+
### Decision Tree
257
+
258
+
```
259
+
Do you need raw CBOR/CAR access?
260
+
โโ Yes โ Use Firehose
261
+
โโ No
262
+
โ
263
+
Do you want server-side filtering?
264
+
โโ Yes โ Use Jetstream (recommended)
265
+
โโ No โ Use Firehose
266
+
```
267
+
268
+
### Use Case Examples
269
+
270
+
**Social Media Analytics (Jetstream)**
271
+
272
+
```php
273
+
// Efficient monitoring with server-side filtering
274
+
public function collections(): ?array
275
+
{
276
+
return [
277
+
'app.bsky.feed.post',
278
+
'app.bsky.feed.like',
279
+
'app.bsky.graph.follow',
280
+
];
281
+
}
282
+
```
283
+
284
+
**Content Moderation (Jetstream)**
285
+
286
+
```php
287
+
// Standard content monitoring
288
+
public function collections(): ?array
289
+
{
290
+
return ['app.bsky.feed.*'];
291
+
}
292
+
```
293
+
294
+
**Comprehensive Indexer (Firehose)**
295
+
296
+
```php
297
+
// Index everything with raw data access
298
+
public function collections(): ?array
299
+
{
300
+
return null; // All collections
301
+
}
302
+
```
303
+
304
+
## Switching Between Modes
305
+
306
+
You can switch modes without code changes:
307
+
308
+
### Option 1: Environment Variable
309
+
310
+
```env
311
+
# Development - comprehensive testing
312
+
SIGNAL_MODE=firehose
313
+
314
+
# Production - efficient processing
315
+
SIGNAL_MODE=jetstream
316
+
```
317
+
318
+
### Option 2: Runtime Configuration
319
+
320
+
```php
321
+
use SocialDept\AtpSignals\Facades\Signal;
322
+
323
+
// Set mode dynamically
324
+
config(['signal.mode' => 'jetstream']);
325
+
326
+
Signal::start();
327
+
```
328
+
329
+
## Performance Comparison
330
+
331
+
### Bandwidth Usage
332
+
333
+
**Processing 1 hour of posts:**
334
+
335
+
| Mode | Data Received | Bandwidth |
336
+
|-----------|-------------------|-----------|
337
+
| Jetstream | ~50,000 events | ~10 MB |
338
+
| Firehose | ~5,000,000 events | ~500 MB |
339
+
340
+
**Savings:** 50x reduction with Jetstream
341
+
342
+
### CPU Usage
343
+
344
+
**Processing same events:**
345
+
346
+
| Mode | CPU Usage | Processing Time |
347
+
|-----------|-----------|-----------------|
348
+
| Jetstream | ~5% | 0.1ms per event |
349
+
| Firehose | ~20% | 0.4ms per event |
350
+
351
+
**Savings:** 4x more efficient with Jetstream
352
+
353
+
### Cost Implications
354
+
355
+
For a medium-traffic application:
356
+
357
+
| Mode | Monthly Bandwidth | Est. Cost* |
358
+
|-----------|-------------------|------------|
359
+
| Jetstream | ~20 GB | ~$2 |
360
+
| Firehose | ~10 TB | ~$1000 |
361
+
362
+
*Estimates vary by provider and usage
363
+
364
+
## Best Practices
365
+
366
+
### Start with Jetstream
367
+
368
+
Start with Jetstream for most applications:
369
+
370
+
```env
371
+
SIGNAL_MODE=jetstream
372
+
```
373
+
374
+
Switch to Firehose only if you need raw CBOR/CAR access.
375
+
376
+
### Use Firehose for Development
377
+
378
+
Test with Firehose in development to see all events:
379
+
380
+
```env
381
+
# .env.local
382
+
SIGNAL_MODE=firehose
383
+
384
+
# .env.production
385
+
SIGNAL_MODE=jetstream
386
+
```
387
+
388
+
### Monitor Performance
389
+
390
+
Track your Signal's performance:
391
+
392
+
```php
393
+
public function handle(SignalEvent $event): void
394
+
{
395
+
$start = microtime(true);
396
+
397
+
// Your logic
398
+
399
+
$duration = microtime(true) - $start;
400
+
401
+
if ($duration > 0.1) {
402
+
Log::warning('Slow signal processing', [
403
+
'signal' => static::class,
404
+
'duration' => $duration,
405
+
'mode' => config('signal.mode'),
406
+
]);
407
+
}
408
+
}
409
+
```
410
+
411
+
### Use Queues with Firehose
412
+
413
+
Firehose generates high volume. Use queues to avoid blocking:
414
+
415
+
```php
416
+
public function shouldQueue(): bool
417
+
{
418
+
// Queue when using Firehose
419
+
return config('signal.mode') === 'firehose';
420
+
}
421
+
```
422
+
423
+
[Learn more about queue integration โ](queues.md)
424
+
425
+
## Testing Both Modes
426
+
427
+
Test your Signals work in both modes:
428
+
429
+
```bash
430
+
# Test with Jetstream
431
+
SIGNAL_MODE=jetstream php artisan signal:test MySignal
432
+
433
+
# Test with Firehose
434
+
SIGNAL_MODE=firehose php artisan signal:test MySignal
435
+
```
436
+
437
+
[Learn more about testing โ](testing.md)
438
+
439
+
## Common Questions
440
+
441
+
### Can I use both modes simultaneously?
442
+
443
+
No, each consumer runs in one mode. However, you can run multiple consumers:
444
+
445
+
```bash
446
+
# Terminal 1 - Jetstream consumer
447
+
SIGNAL_MODE=jetstream php artisan signal:consume
448
+
449
+
# Terminal 2 - Firehose consumer
450
+
SIGNAL_MODE=firehose php artisan signal:consume
451
+
```
452
+
453
+
### Will my Signals break if I switch modes?
454
+
455
+
Signals work in both modes without changes. The main difference is:
456
+
- Jetstream provides server-side filtering (more efficient)
457
+
- Firehose provides raw CBOR/CAR data access (more comprehensive)
458
+
459
+
### How do I know which mode I'm using?
460
+
461
+
Check at runtime:
462
+
463
+
```php
464
+
$mode = config('signal.mode'); // 'jetstream' or 'firehose'
465
+
```
466
+
467
+
Or via Facade:
468
+
469
+
```php
470
+
use SocialDept\AtpSignals\Facades\Signal;
471
+
472
+
$mode = Signal::getMode();
473
+
```
474
+
475
+
### Can I switch modes while consuming?
476
+
477
+
No, you must restart the consumer:
478
+
479
+
```bash
480
+
# Stop current consumer (Ctrl+C)
481
+
482
+
# Change mode
483
+
# Edit .env: SIGNAL_MODE=firehose
484
+
485
+
# Start new consumer
486
+
php artisan signal:consume
487
+
```
488
+
489
+
## Next Steps
490
+
491
+
- **[Learn about queue integration โ](queues.md)** - Handle high-volume events efficiently
492
+
- **[Review configuration options โ](configuration.md)** - Fine-tune your setup
493
+
- **[See real-world examples โ](examples.md)** - Learn from production patterns
+672
docs/queues.md
+672
docs/queues.md
···
1
+
# Queue Integration
2
+
3
+
Processing AT Protocol events can be resource-intensive. Signal's queue integration lets you handle events asynchronously, preventing bottlenecks and improving performance.
4
+
5
+
## Why Use Queues?
6
+
7
+
### Without Queues (Synchronous)
8
+
9
+
```php
10
+
public function handle(SignalEvent $event): void
11
+
{
12
+
$this->performExpensiveAnalysis($event); // Blocks for 2 seconds
13
+
$this->sendNotifications($event); // Blocks for 1 second
14
+
$this->updateDatabase($event); // Blocks for 0.5 seconds
15
+
}
16
+
```
17
+
18
+
**Problems:**
19
+
- Consumer blocks while processing (3.5 seconds per event)
20
+
- Events queue up during slow operations
21
+
- Risk of disconnection during long processing
22
+
- Can't scale horizontally
23
+
- Memory issues with long-running processes
24
+
25
+
### With Queues (Asynchronous)
26
+
27
+
```php
28
+
public function shouldQueue(): bool
29
+
{
30
+
return true;
31
+
}
32
+
33
+
public function handle(SignalEvent $event): void
34
+
{
35
+
$this->performExpensiveAnalysis($event); // Runs in background
36
+
$this->sendNotifications($event); // Runs in background
37
+
$this->updateDatabase($event); // Runs in background
38
+
}
39
+
```
40
+
41
+
**Benefits:**
42
+
- Consumer stays responsive
43
+
- Processing happens in parallel
44
+
- Scale by adding queue workers
45
+
- Better memory management
46
+
- Automatic retry on failures
47
+
48
+
## Basic Queue Configuration
49
+
50
+
### Enable Queueing
51
+
52
+
Simply return `true` from `shouldQueue()`:
53
+
54
+
```php
55
+
class MySignal extends Signal
56
+
{
57
+
public function eventTypes(): array
58
+
{
59
+
return ['commit'];
60
+
}
61
+
62
+
public function shouldQueue(): bool
63
+
{
64
+
return true; // Enable queuing
65
+
}
66
+
67
+
public function handle(SignalEvent $event): void
68
+
{
69
+
// This now runs in a queue job
70
+
}
71
+
}
72
+
```
73
+
74
+
That's it! Signal automatically:
75
+
- Creates a queue job for each event
76
+
- Serializes the event data
77
+
- Dispatches to Laravel's queue system
78
+
- Handles retries and failures
79
+
80
+
### Default Queue Configuration
81
+
82
+
Signal uses your Laravel queue configuration:
83
+
84
+
```env
85
+
# Default queue connection
86
+
QUEUE_CONNECTION=redis
87
+
88
+
# Signal-specific queue (optional)
89
+
SIGNAL_QUEUE=signal
90
+
91
+
# Signal queue connection (optional)
92
+
SIGNAL_QUEUE_CONNECTION=redis
93
+
```
94
+
95
+
## Customizing Queue Behavior
96
+
97
+
### Specify Queue Name
98
+
99
+
Send events to a specific queue:
100
+
101
+
```php
102
+
public function shouldQueue(): bool
103
+
{
104
+
return true;
105
+
}
106
+
107
+
public function queue(): string
108
+
{
109
+
return 'high-priority'; // Queue name
110
+
}
111
+
```
112
+
113
+
Now your events go to the `high-priority` queue:
114
+
115
+
```bash
116
+
php artisan queue:work --queue=high-priority
117
+
```
118
+
119
+
### Specify Queue Connection
120
+
121
+
Use a different queue connection:
122
+
123
+
```php
124
+
public function shouldQueue(): bool
125
+
{
126
+
return true;
127
+
}
128
+
129
+
public function queueConnection(): string
130
+
{
131
+
return 'redis'; // Connection name
132
+
}
133
+
```
134
+
135
+
### Combine Queue Configuration
136
+
137
+
```php
138
+
public function shouldQueue(): bool
139
+
{
140
+
return true;
141
+
}
142
+
143
+
public function queueConnection(): string
144
+
{
145
+
return 'redis';
146
+
}
147
+
148
+
public function queue(): string
149
+
{
150
+
return 'signal-events';
151
+
}
152
+
```
153
+
154
+
## Running Queue Workers
155
+
156
+
### Start a Worker
157
+
158
+
Process queued events:
159
+
160
+
```bash
161
+
php artisan queue:work
162
+
```
163
+
164
+
### Process Specific Queue
165
+
166
+
```bash
167
+
php artisan queue:work --queue=signal
168
+
```
169
+
170
+
### Multiple Queues with Priority
171
+
172
+
Process high-priority queue first:
173
+
174
+
```bash
175
+
php artisan queue:work --queue=high-priority,default
176
+
```
177
+
178
+
### Scale with Multiple Workers
179
+
180
+
Run multiple workers for throughput:
181
+
182
+
```bash
183
+
# Terminal 1
184
+
php artisan queue:work --queue=signal
185
+
186
+
# Terminal 2
187
+
php artisan queue:work --queue=signal
188
+
189
+
# Terminal 3
190
+
php artisan queue:work --queue=signal
191
+
```
192
+
193
+
### Supervisor Configuration
194
+
195
+
For production, use Supervisor to manage workers:
196
+
197
+
```ini
198
+
[program:signal-queue-worker]
199
+
process_name=%(program_name)s_%(process_num)02d
200
+
command=php /path/to/artisan queue:work --sleep=3 --tries=3 --queue=signal
201
+
autostart=true
202
+
autorestart=true
203
+
stopasgroup=true
204
+
killasgroup=true
205
+
user=www-data
206
+
numprocs=4
207
+
redirect_stderr=true
208
+
stdout_logfile=/path/to/logs/signal-worker.log
209
+
stopwaitsecs=3600
210
+
```
211
+
212
+
This creates 4 workers processing the `signal` queue.
213
+
214
+
## Error Handling
215
+
216
+
### Failed Method
217
+
218
+
Handle job failures:
219
+
220
+
```php
221
+
public function shouldQueue(): bool
222
+
{
223
+
return true;
224
+
}
225
+
226
+
public function handle(SignalEvent $event): void
227
+
{
228
+
// Your logic that might fail
229
+
$this->riskyOperation($event);
230
+
}
231
+
232
+
public function failed(SignalEvent $event, \Throwable $exception): void
233
+
{
234
+
Log::error('Signal processing failed', [
235
+
'signal' => static::class,
236
+
'did' => $event->did,
237
+
'collection' => $event->getCollection(),
238
+
'error' => $exception->getMessage(),
239
+
'trace' => $exception->getTraceAsString(),
240
+
]);
241
+
242
+
// Optional: Send alerts
243
+
$this->notifyAdmin($exception);
244
+
245
+
// Optional: Store for manual review
246
+
FailedSignal::create([
247
+
'event_data' => $event->toArray(),
248
+
'exception' => $exception->getMessage(),
249
+
]);
250
+
}
251
+
```
252
+
253
+
### Automatic Retries
254
+
255
+
Laravel automatically retries failed jobs:
256
+
257
+
```bash
258
+
# Retry up to 3 times
259
+
php artisan queue:work --tries=3
260
+
```
261
+
262
+
Configure retry delay:
263
+
264
+
```php
265
+
public function retryAfter(): int
266
+
{
267
+
return 60; // Wait 60 seconds before retry
268
+
}
269
+
```
270
+
271
+
### Exponential Backoff
272
+
273
+
Increase delay between retries:
274
+
275
+
```php
276
+
public function backoff(): array
277
+
{
278
+
return [10, 30, 60]; // 10s, then 30s, then 60s
279
+
}
280
+
```
281
+
282
+
## Performance Optimization
283
+
284
+
### Batch Processing
285
+
286
+
Process multiple events at once:
287
+
288
+
```php
289
+
use Illuminate\Support\Collection;
290
+
291
+
class BatchPostSignal extends Signal
292
+
{
293
+
public function shouldQueue(): bool
294
+
{
295
+
return true;
296
+
}
297
+
298
+
public function handle(SignalEvent $event): void
299
+
{
300
+
// Collect events in cache
301
+
$events = Cache::get('pending_posts', []);
302
+
$events[] = $event->toArray();
303
+
304
+
Cache::put('pending_posts', $events, now()->addMinutes(5));
305
+
306
+
// Process in batches of 100
307
+
if (count($events) >= 100) {
308
+
$this->processBatch($events);
309
+
Cache::forget('pending_posts');
310
+
}
311
+
}
312
+
313
+
private function processBatch(array $events): void
314
+
{
315
+
// Bulk insert, API calls, etc.
316
+
}
317
+
}
318
+
```
319
+
320
+
### Conditional Queuing
321
+
322
+
Queue only expensive operations:
323
+
324
+
```php
325
+
public function shouldQueue(): bool
326
+
{
327
+
// Queue during high traffic
328
+
return now()->hour >= 9 && now()->hour <= 17;
329
+
}
330
+
```
331
+
332
+
Or based on event type:
333
+
334
+
```php
335
+
public function handle(SignalEvent $event): void
336
+
{
337
+
if ($this->isExpensive($event)) {
338
+
dispatch(function () use ($event) {
339
+
$this->handleExpensive($event);
340
+
})->onQueue('slow-operations');
341
+
} else {
342
+
$this->handleQuick($event);
343
+
}
344
+
}
345
+
```
346
+
347
+
### Rate Limiting
348
+
349
+
Prevent overwhelming external APIs:
350
+
351
+
```php
352
+
use Illuminate\Support\Facades\RateLimiter;
353
+
354
+
public function handle(SignalEvent $event): void
355
+
{
356
+
RateLimiter::attempt(
357
+
'api-calls',
358
+
$perMinute = 100,
359
+
function () use ($event) {
360
+
$this->callExternalAPI($event);
361
+
}
362
+
);
363
+
}
364
+
```
365
+
366
+
## Common Patterns
367
+
368
+
### High-Volume Signal
369
+
370
+
Process millions of events efficiently:
371
+
372
+
```php
373
+
class HighVolumeSignal extends Signal
374
+
{
375
+
public function eventTypes(): array
376
+
{
377
+
return ['commit'];
378
+
}
379
+
380
+
public function collections(): ?array
381
+
{
382
+
return ['app.bsky.feed.post'];
383
+
}
384
+
385
+
public function shouldQueue(): bool
386
+
{
387
+
return true;
388
+
}
389
+
390
+
public function queue(): string
391
+
{
392
+
return 'high-volume';
393
+
}
394
+
395
+
public function handle(SignalEvent $event): void
396
+
{
397
+
// Lightweight processing only
398
+
$this->incrementCounter($event);
399
+
}
400
+
}
401
+
```
402
+
403
+
Run many workers:
404
+
405
+
```bash
406
+
# 10 workers on high-volume queue
407
+
php artisan queue:work --queue=high-volume --workers=10
408
+
```
409
+
410
+
### Priority Queues
411
+
412
+
Different priorities for different events:
413
+
414
+
```php
415
+
class PrioritySignal extends Signal
416
+
{
417
+
public function shouldQueue(): bool
418
+
{
419
+
return true;
420
+
}
421
+
422
+
public function queue(): string
423
+
{
424
+
// Determine priority based on event
425
+
return $this->getQueueForEvent();
426
+
}
427
+
428
+
private function getQueueForEvent(): string
429
+
{
430
+
// Check event attributes
431
+
// Return 'high', 'medium', or 'low'
432
+
}
433
+
}
434
+
```
435
+
436
+
Process high-priority first:
437
+
438
+
```bash
439
+
php artisan queue:work --queue=high,medium,low
440
+
```
441
+
442
+
### Delayed Processing
443
+
444
+
Delay event processing:
445
+
446
+
```php
447
+
public function handle(SignalEvent $event): void
448
+
{
449
+
// Dispatch with delay
450
+
dispatch(function () use ($event) {
451
+
$this->processLater($event);
452
+
})->delay(now()->addMinutes(5));
453
+
}
454
+
```
455
+
456
+
### Scheduled Batch Processing
457
+
458
+
Collect events and process on schedule:
459
+
460
+
```php
461
+
// Signal collects events
462
+
class CollectorSignal extends Signal
463
+
{
464
+
public function handle(SignalEvent $event): void
465
+
{
466
+
PendingEvent::create([
467
+
'data' => $event->toArray(),
468
+
]);
469
+
}
470
+
}
471
+
472
+
// Scheduled command processes batch
473
+
// app/Console/Kernel.php
474
+
protected function schedule(Schedule $schedule)
475
+
{
476
+
$schedule->call(function () {
477
+
$events = PendingEvent::all();
478
+
$this->processBatch($events);
479
+
PendingEvent::truncate();
480
+
})->hourly();
481
+
}
482
+
```
483
+
484
+
## Monitoring Queues
485
+
486
+
### Check Queue Status
487
+
488
+
```bash
489
+
# View failed jobs
490
+
php artisan queue:failed
491
+
492
+
# Retry failed job
493
+
php artisan queue:retry {id}
494
+
495
+
# Retry all failed
496
+
php artisan queue:retry all
497
+
498
+
# Clear failed jobs
499
+
php artisan queue:flush
500
+
```
501
+
502
+
### Queue Metrics
503
+
504
+
Track queue performance:
505
+
506
+
```php
507
+
use Illuminate\Support\Facades\Queue;
508
+
509
+
Queue::after(function ($connection, $job, $data) {
510
+
Log::info('Job processed', [
511
+
'queue' => $job->queue,
512
+
'class' => $job->resolveName(),
513
+
'attempts' => $job->attempts(),
514
+
]);
515
+
});
516
+
```
517
+
518
+
### Horizon (Recommended)
519
+
520
+
Use Laravel Horizon for Redis queues:
521
+
522
+
```bash
523
+
composer require laravel/horizon
524
+
php artisan horizon:install
525
+
php artisan horizon
526
+
```
527
+
528
+
View dashboard at `/horizon`.
529
+
530
+
## Testing Queued Signals
531
+
532
+
### Test with Fake Queue
533
+
534
+
```php
535
+
use Illuminate\Support\Facades\Queue;
536
+
537
+
/** @test */
538
+
public function it_queues_events()
539
+
{
540
+
Queue::fake();
541
+
542
+
$signal = new MySignal();
543
+
$event = $this->createSampleEvent();
544
+
545
+
// Assert queue behavior
546
+
$this->assertTrue($signal->shouldQueue());
547
+
548
+
// Process would normally queue
549
+
$signal->handle($event);
550
+
551
+
// Verify job was queued
552
+
Queue::assertPushed(SignalJob::class);
553
+
}
554
+
```
555
+
556
+
### Test Synchronously
557
+
558
+
Disable queueing for tests:
559
+
560
+
```php
561
+
/** @test */
562
+
public function it_processes_events()
563
+
{
564
+
config(['queue.default' => 'sync']);
565
+
566
+
$signal = new MySignal();
567
+
$event = $this->createSampleEvent();
568
+
569
+
$signal->handle($event);
570
+
571
+
// Assert processing happened
572
+
$this->assertDatabaseHas('posts', [...]);
573
+
}
574
+
```
575
+
576
+
[Learn more about testing โ](testing.md)
577
+
578
+
## Production Checklist
579
+
580
+
### Infrastructure
581
+
582
+
- [ ] Queue driver configured (Redis recommended)
583
+
- [ ] Supervisor installed and configured
584
+
- [ ] Multiple workers running
585
+
- [ ] Worker auto-restart enabled
586
+
- [ ] Logs configured and monitored
587
+
588
+
### Configuration
589
+
590
+
- [ ] Queue connection set correctly
591
+
- [ ] Queue names configured
592
+
- [ ] Retry attempts configured
593
+
- [ ] Timeout values appropriate
594
+
- [ ] Memory limits set
595
+
596
+
### Monitoring
597
+
598
+
- [ ] Queue length monitored
599
+
- [ ] Failed jobs tracked
600
+
- [ ] Worker health checked
601
+
- [ ] Processing times measured
602
+
- [ ] Horizon installed (if using Redis)
603
+
604
+
### Scaling
605
+
606
+
- [ ] Worker count appropriate for volume
607
+
- [ ] Priority queues configured
608
+
- [ ] Rate limiting implemented
609
+
- [ ] Database connection pooling enabled
610
+
- [ ] Redis maxmemory policy set
611
+
612
+
## Common Issues
613
+
614
+
### Queue Jobs Not Processing
615
+
616
+
**Check worker is running:**
617
+
```bash
618
+
php artisan queue:work
619
+
```
620
+
621
+
**Check queue connection:**
622
+
```php
623
+
// Should match QUEUE_CONNECTION
624
+
config('queue.default')
625
+
```
626
+
627
+
### Jobs Timing Out
628
+
629
+
**Increase timeout:**
630
+
```bash
631
+
php artisan queue:work --timeout=300
632
+
```
633
+
634
+
**Or in Signal:**
635
+
```php
636
+
public function timeout(): int
637
+
{
638
+
return 300; // 5 minutes
639
+
}
640
+
```
641
+
642
+
### Memory Leaks
643
+
644
+
**Restart workers periodically:**
645
+
```bash
646
+
php artisan queue:work --max-jobs=1000
647
+
```
648
+
649
+
Or:
650
+
```bash
651
+
php artisan queue:work --max-time=3600
652
+
```
653
+
654
+
### Failed Jobs Piling Up
655
+
656
+
**Review failures:**
657
+
```bash
658
+
php artisan queue:failed
659
+
```
660
+
661
+
**Retry or delete:**
662
+
```bash
663
+
php artisan queue:retry all
664
+
# or
665
+
php artisan queue:flush
666
+
```
667
+
668
+
## Next Steps
669
+
670
+
- **[Review configuration options โ](configuration.md)** - Fine-tune queue settings
671
+
- **[Learn about testing โ](testing.md)** - Test queued Signals
672
+
- **[See real-world examples โ](examples.md)** - Learn from production queue patterns
+391
docs/quickstart.md
+391
docs/quickstart.md
···
1
+
# Quickstart Guide
2
+
3
+
This guide will walk you through building your first Signal and consuming AT Protocol events in under 5 minutes.
4
+
5
+
## Prerequisites
6
+
7
+
Before starting, ensure you have:
8
+
9
+
- [Installed Signal](installation.md) in your Laravel application
10
+
- Run `php artisan signal:install` successfully
11
+
- Basic familiarity with Laravel
12
+
13
+
## Your First Signal
14
+
15
+
We'll build a Signal that logs every new post created on Bluesky.
16
+
17
+
### Step 1: Generate a Signal
18
+
19
+
Use the Artisan command to create a new Signal:
20
+
21
+
```bash
22
+
php artisan make:signal NewPostSignal
23
+
```
24
+
25
+
This creates `app/Signals/NewPostSignal.php` with a basic template.
26
+
27
+
### Step 2: Define the Signal
28
+
29
+
Open the generated file and update it:
30
+
31
+
```php
32
+
<?php
33
+
34
+
namespace App\Signals;
35
+
36
+
use SocialDept\AtpSignals\Events\SignalEvent;
37
+
use SocialDept\AtpSignals\Signals\Signal;
38
+
use Illuminate\Support\Facades\Log;
39
+
40
+
class NewPostSignal extends Signal
41
+
{
42
+
/**
43
+
* Define which event types to listen for.
44
+
*/
45
+
public function eventTypes(): array
46
+
{
47
+
return ['commit']; // Listen for repository commits
48
+
}
49
+
50
+
/**
51
+
* Filter by specific collections.
52
+
*/
53
+
public function collections(): ?array
54
+
{
55
+
return ['app.bsky.feed.post']; // Only handle posts
56
+
}
57
+
58
+
/**
59
+
* Handle the event when it arrives.
60
+
*/
61
+
public function handle(SignalEvent $event): void
62
+
{
63
+
$record = $event->getRecord();
64
+
65
+
Log::info('New post created', [
66
+
'author' => $event->did,
67
+
'text' => $record->text ?? null,
68
+
'created_at' => $record->createdAt ?? null,
69
+
]);
70
+
}
71
+
}
72
+
```
73
+
74
+
### Step 3: Start Consuming Events
75
+
76
+
Run the consumer to start listening:
77
+
78
+
```bash
79
+
php artisan signal:consume
80
+
```
81
+
82
+
You should see output like:
83
+
84
+
```
85
+
Starting Signal consumer in jetstream mode...
86
+
Connecting to wss://jetstream2.us-east.bsky.network...
87
+
Connected! Listening for events...
88
+
```
89
+
90
+
**Congratulations!** Your Signal is now processing every new post on Bluesky in real-time. Check your Laravel logs to see the posts coming in.
91
+
92
+
## Understanding What Just Happened
93
+
94
+
Let's break down the Signal you created:
95
+
96
+
### Event Types
97
+
98
+
```php
99
+
public function eventTypes(): array
100
+
{
101
+
return ['commit'];
102
+
}
103
+
```
104
+
105
+
This tells Signal you want **commit** events, which represent changes to repositories (like creating posts, likes, follows, etc.).
106
+
107
+
Available event types:
108
+
- `commit` - Repository commits (most common)
109
+
- `identity` - Identity changes (handle updates)
110
+
- `account` - Account status changes
111
+
112
+
### Collections
113
+
114
+
```php
115
+
public function collections(): ?array
116
+
{
117
+
return ['app.bsky.feed.post'];
118
+
}
119
+
```
120
+
121
+
This filters to only **post** collections. Without this filter, your Signal would receive all commit events for every collection type.
122
+
123
+
Common collections:
124
+
- `app.bsky.feed.post` - Posts
125
+
- `app.bsky.feed.like` - Likes
126
+
- `app.bsky.graph.follow` - Follows
127
+
- `app.bsky.feed.repost` - Reposts
128
+
129
+
[Learn more about filtering โ](filtering.md)
130
+
131
+
### Handler Method
132
+
133
+
```php
134
+
public function handle(SignalEvent $event): void
135
+
{
136
+
$record = $event->getRecord();
137
+
// Your logic here
138
+
}
139
+
```
140
+
141
+
This is where your code runs for each matching event. The `$event` object contains:
142
+
143
+
- `did` - The user's DID (decentralized identifier)
144
+
- `timeUs` - Timestamp in microseconds
145
+
- `commit` - Commit details (collection, operation, record key)
146
+
- `getRecord()` - The actual record data
147
+
148
+
## Next Steps
149
+
150
+
Now that you've built your first Signal, let's make it more useful.
151
+
152
+
### Add More Filtering
153
+
154
+
Track specific operations only:
155
+
156
+
```php
157
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
158
+
159
+
public function operations(): ?array
160
+
{
161
+
return [SignalCommitOperation::Create]; // Only new posts, not edits
162
+
}
163
+
```
164
+
165
+
[Learn more about filtering โ](filtering.md)
166
+
167
+
### Process Events Asynchronously
168
+
169
+
For expensive operations, use Laravel queues:
170
+
171
+
```php
172
+
public function shouldQueue(): bool
173
+
{
174
+
return true;
175
+
}
176
+
177
+
public function handle(SignalEvent $event): void
178
+
{
179
+
// This now runs in a background job
180
+
$this->performExpensiveAnalysis($event);
181
+
}
182
+
```
183
+
184
+
[Learn more about queues โ](queues.md)
185
+
186
+
### Store Data
187
+
188
+
Let's store posts in your database:
189
+
190
+
```php
191
+
use App\Models\Post;
192
+
193
+
public function handle(SignalEvent $event): void
194
+
{
195
+
$record = $event->getRecord();
196
+
197
+
Post::updateOrCreate(
198
+
[
199
+
'did' => $event->did,
200
+
'rkey' => $event->commit->rkey,
201
+
],
202
+
[
203
+
'text' => $record->text ?? null,
204
+
'created_at' => $record->createdAt,
205
+
]
206
+
);
207
+
}
208
+
```
209
+
210
+
### Handle Multiple Collections
211
+
212
+
Use wildcards to match multiple collections:
213
+
214
+
```php
215
+
public function collections(): ?array
216
+
{
217
+
return [
218
+
'app.bsky.feed.*', // All feed events
219
+
];
220
+
}
221
+
222
+
public function handle(SignalEvent $event): void
223
+
{
224
+
$collection = $event->getCollection();
225
+
226
+
match ($collection) {
227
+
'app.bsky.feed.post' => $this->handlePost($event),
228
+
'app.bsky.feed.like' => $this->handleLike($event),
229
+
'app.bsky.feed.repost' => $this->handleRepost($event),
230
+
default => null,
231
+
};
232
+
}
233
+
```
234
+
235
+
## Building Something Real
236
+
237
+
Let's build a simple engagement tracker:
238
+
239
+
```php
240
+
<?php
241
+
242
+
namespace App\Signals;
243
+
244
+
use App\Models\EngagementMetric;
245
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
246
+
use SocialDept\AtpSignals\Events\SignalEvent;
247
+
use SocialDept\AtpSignals\Signals\Signal;
248
+
249
+
class EngagementTrackerSignal extends Signal
250
+
{
251
+
public function eventTypes(): array
252
+
{
253
+
return ['commit'];
254
+
}
255
+
256
+
public function collections(): ?array
257
+
{
258
+
return [
259
+
'app.bsky.feed.post',
260
+
'app.bsky.feed.like',
261
+
'app.bsky.feed.repost',
262
+
];
263
+
}
264
+
265
+
public function operations(): ?array
266
+
{
267
+
return [SignalCommitOperation::Create];
268
+
}
269
+
270
+
public function shouldQueue(): bool
271
+
{
272
+
return true; // Process in background
273
+
}
274
+
275
+
public function handle(SignalEvent $event): void
276
+
{
277
+
EngagementMetric::create([
278
+
'date' => now()->toDateString(),
279
+
'collection' => $event->getCollection(),
280
+
'event_type' => 'create',
281
+
'count' => 1,
282
+
]);
283
+
}
284
+
}
285
+
```
286
+
287
+
This Signal tracks all engagement activity (posts, likes, reposts) and stores metrics for analysis.
288
+
289
+
## Testing Your Signal
290
+
291
+
Before running in production, test your Signal with sample data:
292
+
293
+
```bash
294
+
php artisan signal:test NewPostSignal
295
+
```
296
+
297
+
This will run your Signal with a sample event and show you the output.
298
+
299
+
[Learn more about testing โ](testing.md)
300
+
301
+
## Common Patterns
302
+
303
+
### Only Process Specific Users
304
+
305
+
```php
306
+
public function dids(): ?array
307
+
{
308
+
return [
309
+
'did:plc:z72i7hdynmk6r22z27h6tvur', // Specific user
310
+
];
311
+
}
312
+
```
313
+
314
+
### Add Custom Filtering Logic
315
+
316
+
```php
317
+
public function shouldHandle(SignalEvent $event): bool
318
+
{
319
+
$record = $event->getRecord();
320
+
321
+
// Only handle posts with images
322
+
return isset($record->embed);
323
+
}
324
+
```
325
+
326
+
### Handle Failures Gracefully
327
+
328
+
```php
329
+
public function failed(SignalEvent $event, \Throwable $exception): void
330
+
{
331
+
Log::error('Signal processing failed', [
332
+
'event' => $event->toArray(),
333
+
'error' => $exception->getMessage(),
334
+
]);
335
+
336
+
// Optionally notify admins, store for retry, etc.
337
+
}
338
+
```
339
+
340
+
## Running in Production
341
+
342
+
### Using Supervisor
343
+
344
+
For production, run Signal under a process monitor like Supervisor:
345
+
346
+
```ini
347
+
[program:signal-consumer]
348
+
process_name=%(program_name)s
349
+
command=php /path/to/artisan signal:consume
350
+
autostart=true
351
+
autorestart=true
352
+
user=www-data
353
+
redirect_stderr=true
354
+
stdout_logfile=/path/to/logs/signal-consumer.log
355
+
```
356
+
357
+
### Starting from Last Position
358
+
359
+
Signal automatically saves cursor positions, so it resumes from where it left off:
360
+
361
+
```bash
362
+
php artisan signal:consume
363
+
```
364
+
365
+
To start fresh and ignore stored position:
366
+
367
+
```bash
368
+
php artisan signal:consume --fresh
369
+
```
370
+
371
+
To start from a specific cursor:
372
+
373
+
```bash
374
+
php artisan signal:consume --cursor=123456789
375
+
```
376
+
377
+
## What's Next?
378
+
379
+
You now know the basics of building Signals! Explore more advanced topics:
380
+
381
+
- **[Signal Architecture](signals.md)** - Deep dive into Signal structure
382
+
- **[Advanced Filtering](filtering.md)** - Master collection patterns and wildcards
383
+
- **[Jetstream vs Firehose](modes.md)** - Choose the right mode for your use case
384
+
- **[Queue Integration](queues.md)** - Build high-performance processors
385
+
- **[Real-World Examples](examples.md)** - Learn from production use cases
386
+
387
+
## Getting Help
388
+
389
+
- Check the [examples documentation](examples.md) for more patterns
390
+
- Review the [configuration guide](configuration.md) for all options
391
+
- Open an issue on GitHub if you encounter problems
+702
docs/signals.md
+702
docs/signals.md
···
1
+
# Creating Signals
2
+
3
+
Signals are the heart of the Signal package. They define how your application responds to AT Protocol events.
4
+
5
+
## What is a Signal?
6
+
7
+
A **Signal** is a PHP class that:
8
+
9
+
1. Listens for specific types of AT Protocol events
10
+
2. Filters those events based on your criteria
11
+
3. Executes custom logic when matching events arrive
12
+
13
+
Think of Signals like Laravel event listeners, but specifically designed for the AT Protocol.
14
+
15
+
## Basic Signal Structure
16
+
17
+
Every Signal extends the base `Signal` class:
18
+
19
+
```php
20
+
<?php
21
+
22
+
namespace App\Signals;
23
+
24
+
use SocialDept\AtpSignals\Events\SignalEvent;
25
+
use SocialDept\AtpSignals\Signals\Signal;
26
+
27
+
class MySignal extends Signal
28
+
{
29
+
/**
30
+
* Define which event types to listen for.
31
+
* Required.
32
+
*/
33
+
public function eventTypes(): array
34
+
{
35
+
return ['commit'];
36
+
}
37
+
38
+
/**
39
+
* Handle the event when it arrives.
40
+
* Required.
41
+
*/
42
+
public function handle(SignalEvent $event): void
43
+
{
44
+
// Your logic here
45
+
}
46
+
}
47
+
```
48
+
49
+
Only two methods are required:
50
+
- `eventTypes()` - Which event types to listen for
51
+
- `handle()` - What to do when events arrive
52
+
53
+
## Creating Signals
54
+
55
+
### Using Artisan (Recommended)
56
+
57
+
Generate a new Signal with the make command:
58
+
59
+
```bash
60
+
php artisan make:signal MySignal
61
+
```
62
+
63
+
This creates `app/Signals/MySignal.php` with a basic template.
64
+
65
+
#### With Options
66
+
67
+
Generate a Signal with pre-configured filters:
68
+
69
+
```bash
70
+
# Create a Signal for posts only
71
+
php artisan make:signal PostSignal --type=commit --collection=app.bsky.feed.post
72
+
73
+
# Create a Signal for follows
74
+
php artisan make:signal FollowSignal --type=commit --collection=app.bsky.graph.follow
75
+
```
76
+
77
+
### Manual Creation
78
+
79
+
You can also create Signals manually in `app/Signals/`:
80
+
81
+
```php
82
+
<?php
83
+
84
+
namespace App\Signals;
85
+
86
+
use SocialDept\AtpSignals\Events\SignalEvent;
87
+
use SocialDept\AtpSignals\Signals\Signal;
88
+
89
+
class ManualSignal extends Signal
90
+
{
91
+
public function eventTypes(): array
92
+
{
93
+
return ['commit'];
94
+
}
95
+
96
+
public function handle(SignalEvent $event): void
97
+
{
98
+
//
99
+
}
100
+
}
101
+
```
102
+
103
+
Signals are automatically discovered from `app/Signals/` - no registration needed.
104
+
105
+
## Event Types
106
+
107
+
Signals can listen for three types of AT Protocol events:
108
+
109
+
### Commit Events
110
+
111
+
Repository commits represent changes to user data:
112
+
113
+
```php
114
+
use SocialDept\AtpSignals\Enums\SignalEventType;
115
+
116
+
public function eventTypes(): array
117
+
{
118
+
return [SignalEventType::Commit];
119
+
// Or: return ['commit'];
120
+
}
121
+
```
122
+
123
+
**Common commit events:**
124
+
- Creating posts, likes, follows, reposts
125
+
- Updating profile information
126
+
- Deleting content
127
+
128
+
This is the most common event type and what you'll use 99% of the time.
129
+
130
+
### Identity Events
131
+
132
+
Identity changes track handle updates:
133
+
134
+
```php
135
+
public function eventTypes(): array
136
+
{
137
+
return [SignalEventType::Identity];
138
+
// Or: return ['identity'];
139
+
}
140
+
```
141
+
142
+
**Use cases:**
143
+
- Tracking handle changes
144
+
- Updating local user records
145
+
- Monitoring account migrations
146
+
147
+
### Account Events
148
+
149
+
Account status changes track account state:
150
+
151
+
```php
152
+
public function eventTypes(): array
153
+
{
154
+
return [SignalEventType::Account];
155
+
// Or: return ['account'];
156
+
}
157
+
```
158
+
159
+
**Use cases:**
160
+
- Detecting account deactivation
161
+
- Monitoring account status
162
+
- Compliance tracking
163
+
164
+
### Multiple Event Types
165
+
166
+
Listen to multiple event types in one Signal:
167
+
168
+
```php
169
+
public function eventTypes(): array
170
+
{
171
+
return [
172
+
SignalEventType::Commit,
173
+
SignalEventType::Identity,
174
+
];
175
+
}
176
+
177
+
public function handle(SignalEvent $event): void
178
+
{
179
+
if ($event->isCommit()) {
180
+
// Handle commit
181
+
}
182
+
183
+
if ($event->isIdentity()) {
184
+
// Handle identity change
185
+
}
186
+
}
187
+
```
188
+
189
+
## The SignalEvent Object
190
+
191
+
The `SignalEvent` object contains all event data:
192
+
193
+
### Common Properties
194
+
195
+
```php
196
+
public function handle(SignalEvent $event): void
197
+
{
198
+
// User's DID (decentralized identifier)
199
+
$did = $event->did; // "did:plc:z72i7hdynmk6r22z27h6tvur"
200
+
201
+
// Event type (commit, identity, account)
202
+
$kind = $event->kind;
203
+
204
+
// Timestamp in microseconds
205
+
$timestamp = $event->timeUs;
206
+
207
+
// Convert to Carbon instance
208
+
$date = $event->getTimestamp();
209
+
}
210
+
```
211
+
212
+
### Commit Events
213
+
214
+
For commit events, access the `commit` property:
215
+
216
+
```php
217
+
public function handle(SignalEvent $event): void
218
+
{
219
+
if ($event->isCommit()) {
220
+
// Collection (e.g., "app.bsky.feed.post")
221
+
$collection = $event->commit->collection;
222
+
// Or: $collection = $event->getCollection();
223
+
224
+
// Operation (create, update, delete)
225
+
$operation = $event->commit->operation;
226
+
// Or: $operation = $event->getOperation();
227
+
228
+
// Record key (unique identifier)
229
+
$rkey = $event->commit->rkey;
230
+
231
+
// Revision
232
+
$rev = $event->commit->rev;
233
+
234
+
// The actual record data
235
+
$record = $event->commit->record;
236
+
// Or: $record = $event->getRecord();
237
+
}
238
+
}
239
+
```
240
+
241
+
### Working with Records
242
+
243
+
Records contain the actual data (posts, likes, etc.):
244
+
245
+
```php
246
+
public function handle(SignalEvent $event): void
247
+
{
248
+
$record = $event->getRecord();
249
+
250
+
// For posts (app.bsky.feed.post)
251
+
$text = $record->text ?? null;
252
+
$createdAt = $record->createdAt ?? null;
253
+
$embed = $record->embed ?? null;
254
+
$facets = $record->facets ?? null;
255
+
256
+
// For likes (app.bsky.feed.like)
257
+
$subject = $record->subject ?? null;
258
+
259
+
// For follows (app.bsky.graph.follow)
260
+
$subject = $record->subject ?? null;
261
+
}
262
+
```
263
+
264
+
Records are `stdClass` objects, so use null coalescing (`??`) for safety.
265
+
266
+
### Identity Events
267
+
268
+
For identity events, access the `identity` property:
269
+
270
+
```php
271
+
public function handle(SignalEvent $event): void
272
+
{
273
+
if ($event->isIdentity()) {
274
+
// New handle
275
+
$handle = $event->identity->handle;
276
+
277
+
// User's DID
278
+
$did = $event->did;
279
+
280
+
// Sequence number
281
+
$seq = $event->identity->seq;
282
+
283
+
// Timestamp
284
+
$time = $event->identity->time;
285
+
}
286
+
}
287
+
```
288
+
289
+
### Account Events
290
+
291
+
For account events, access the `account` property:
292
+
293
+
```php
294
+
public function handle(SignalEvent $event): void
295
+
{
296
+
if ($event->isAccount()) {
297
+
// Account status
298
+
$active = $event->account->active; // true/false
299
+
300
+
// Status reason
301
+
$status = $event->account->status ?? null;
302
+
303
+
// User's DID
304
+
$did = $event->did;
305
+
306
+
// Sequence number
307
+
$seq = $event->account->seq;
308
+
309
+
// Timestamp
310
+
$time = $event->account->time;
311
+
}
312
+
}
313
+
```
314
+
315
+
## Helper Methods
316
+
317
+
Signals provide several helper methods for common tasks:
318
+
319
+
### Type Checking
320
+
321
+
```php
322
+
public function handle(SignalEvent $event): void
323
+
{
324
+
// Check event type
325
+
if ($event->isCommit()) {
326
+
//
327
+
}
328
+
329
+
if ($event->isIdentity()) {
330
+
//
331
+
}
332
+
333
+
if ($event->isAccount()) {
334
+
//
335
+
}
336
+
}
337
+
```
338
+
339
+
### Operation Checking (Commit Events)
340
+
341
+
```php
342
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
343
+
344
+
public function handle(SignalEvent $event): void
345
+
{
346
+
$operation = $event->getOperation();
347
+
348
+
// Using enum
349
+
if ($operation === SignalCommitOperation::Create) {
350
+
// Handle new records
351
+
}
352
+
353
+
// Using commit helper
354
+
if ($event->commit->isCreate()) {
355
+
// Handle new records
356
+
}
357
+
358
+
if ($event->commit->isUpdate()) {
359
+
// Handle updates
360
+
}
361
+
362
+
if ($event->commit->isDelete()) {
363
+
// Handle deletions
364
+
}
365
+
}
366
+
```
367
+
368
+
### Data Extraction
369
+
370
+
```php
371
+
public function handle(SignalEvent $event): void
372
+
{
373
+
// Get collection (commit events only)
374
+
$collection = $event->getCollection();
375
+
376
+
// Get operation (commit events only)
377
+
$operation = $event->getOperation();
378
+
379
+
// Get record (commit events only)
380
+
$record = $event->getRecord();
381
+
382
+
// Get timestamp as Carbon
383
+
$timestamp = $event->getTimestamp();
384
+
385
+
// Convert to array
386
+
$array = $event->toArray();
387
+
}
388
+
```
389
+
390
+
## Optional Signal Methods
391
+
392
+
Signals support several optional methods for advanced behavior:
393
+
394
+
### Collections Filter
395
+
396
+
Filter by AT Protocol collections:
397
+
398
+
```php
399
+
public function collections(): ?array
400
+
{
401
+
return ['app.bsky.feed.post'];
402
+
}
403
+
```
404
+
405
+
Return `null` to handle all collections.
406
+
407
+
[Learn more about collection filtering โ](filtering.md)
408
+
409
+
### Operations Filter
410
+
411
+
Filter by operation type (commit events only):
412
+
413
+
```php
414
+
public function operations(): ?array
415
+
{
416
+
return [SignalCommitOperation::Create];
417
+
}
418
+
```
419
+
420
+
Return `null` to handle all operations.
421
+
422
+
[Learn more about operation filtering โ](filtering.md)
423
+
424
+
### DIDs Filter
425
+
426
+
Filter by specific users:
427
+
428
+
```php
429
+
public function dids(): ?array
430
+
{
431
+
return [
432
+
'did:plc:z72i7hdynmk6r22z27h6tvur',
433
+
];
434
+
}
435
+
```
436
+
437
+
Return `null` to handle all users.
438
+
439
+
[Learn more about DID filtering โ](filtering.md)
440
+
441
+
### Custom Filtering
442
+
443
+
Add complex filtering logic:
444
+
445
+
```php
446
+
public function shouldHandle(SignalEvent $event): bool
447
+
{
448
+
// Only handle posts with images
449
+
if ($event->isCommit() && $event->getCollection() === 'app.bsky.feed.post') {
450
+
$record = $event->getRecord();
451
+
return isset($record->embed);
452
+
}
453
+
454
+
return true;
455
+
}
456
+
```
457
+
458
+
### Queue Configuration
459
+
460
+
Process events asynchronously:
461
+
462
+
```php
463
+
public function shouldQueue(): bool
464
+
{
465
+
return true;
466
+
}
467
+
468
+
public function queue(): string
469
+
{
470
+
return 'high-priority';
471
+
}
472
+
473
+
public function queueConnection(): string
474
+
{
475
+
return 'redis';
476
+
}
477
+
```
478
+
479
+
[Learn more about queue integration โ](queues.md)
480
+
481
+
### Failure Handling
482
+
483
+
Handle processing failures:
484
+
485
+
```php
486
+
public function failed(SignalEvent $event, \Throwable $exception): void
487
+
{
488
+
Log::error('Signal failed', [
489
+
'signal' => static::class,
490
+
'event' => $event->toArray(),
491
+
'error' => $exception->getMessage(),
492
+
]);
493
+
}
494
+
```
495
+
496
+
## Signal Lifecycle
497
+
498
+
Understanding the Signal lifecycle helps you write better Signals:
499
+
500
+
### 1. Event Arrives
501
+
502
+
An event arrives from the AT Protocol (via Jetstream or Firehose).
503
+
504
+
### 2. Event Type Matching
505
+
506
+
Signal checks if the event type matches your `eventTypes()` definition.
507
+
508
+
### 3. Collection Filtering
509
+
510
+
If defined, Signal checks if the collection matches your `collections()` definition.
511
+
512
+
### 4. Operation Filtering
513
+
514
+
If defined, Signal checks if the operation matches your `operations()` definition.
515
+
516
+
### 5. DID Filtering
517
+
518
+
If defined, Signal checks if the DID matches your `dids()` definition.
519
+
520
+
### 6. Custom Filtering
521
+
522
+
If defined, Signal calls your `shouldHandle()` method.
523
+
524
+
### 7. Queue Decision
525
+
526
+
Signal checks `shouldQueue()` to determine if the event should be queued.
527
+
528
+
### 8. Handler Execution
529
+
530
+
Your `handle()` method is called (either synchronously or via queue).
531
+
532
+
### 9. Failure Handling (if applicable)
533
+
534
+
If an exception occurs, your `failed()` method is called (if defined).
535
+
536
+
## Best Practices
537
+
538
+
### Keep Handlers Focused
539
+
540
+
Each Signal should do one thing well:
541
+
542
+
```php
543
+
// Good - focused on one task
544
+
class TrackNewPostsSignal extends Signal
545
+
{
546
+
public function collections(): ?array
547
+
{
548
+
return ['app.bsky.feed.post'];
549
+
}
550
+
551
+
public function handle(SignalEvent $event): void
552
+
{
553
+
$this->storePost($event);
554
+
}
555
+
}
556
+
557
+
// Less ideal - doing too much
558
+
class MonitorEverythingSignal extends Signal
559
+
{
560
+
public function handle(SignalEvent $event): void
561
+
{
562
+
$this->storePost($event);
563
+
$this->sendNotification($event);
564
+
$this->updateAnalytics($event);
565
+
$this->processRecommendations($event);
566
+
}
567
+
}
568
+
```
569
+
570
+
### Use Queues for Heavy Work
571
+
572
+
Don't block the consumer with expensive operations:
573
+
574
+
```php
575
+
class AnalyzePostSignal extends Signal
576
+
{
577
+
public function shouldQueue(): bool
578
+
{
579
+
return true; // Process in background
580
+
}
581
+
582
+
public function handle(SignalEvent $event): void
583
+
{
584
+
$this->performExpensiveAnalysis($event);
585
+
}
586
+
}
587
+
```
588
+
589
+
### Validate Data Safely
590
+
591
+
Records can have missing or unexpected data:
592
+
593
+
```php
594
+
public function handle(SignalEvent $event): void
595
+
{
596
+
$record = $event->getRecord();
597
+
598
+
// Use null coalescing
599
+
$text = $record->text ?? '';
600
+
601
+
// Validate before processing
602
+
if (empty($text)) {
603
+
return;
604
+
}
605
+
606
+
// Safe to process
607
+
$this->processText($text);
608
+
}
609
+
```
610
+
611
+
### Add Logging
612
+
613
+
Log important events for debugging:
614
+
615
+
```php
616
+
public function handle(SignalEvent $event): void
617
+
{
618
+
Log::debug('Processing event', [
619
+
'signal' => static::class,
620
+
'collection' => $event->getCollection(),
621
+
'operation' => $event->getOperation()->value,
622
+
]);
623
+
624
+
// Your logic
625
+
}
626
+
```
627
+
628
+
### Handle Failures Gracefully
629
+
630
+
Always implement failure handling for queued Signals:
631
+
632
+
```php
633
+
public function failed(SignalEvent $event, \Throwable $exception): void
634
+
{
635
+
Log::error('Signal processing failed', [
636
+
'signal' => static::class,
637
+
'event_did' => $event->did,
638
+
'error' => $exception->getMessage(),
639
+
'trace' => $exception->getTraceAsString(),
640
+
]);
641
+
642
+
// Optionally: send to error tracking service
643
+
// report($exception);
644
+
}
645
+
```
646
+
647
+
## Auto-Discovery
648
+
649
+
Signals are automatically discovered from `app/Signals/` by default. You can customize discovery in `config/signal.php`:
650
+
651
+
```php
652
+
'auto_discovery' => [
653
+
'enabled' => true,
654
+
'path' => app_path('Signals'),
655
+
'namespace' => 'App\\Signals',
656
+
],
657
+
```
658
+
659
+
### Manual Registration
660
+
661
+
Disable auto-discovery and register Signals manually:
662
+
663
+
```php
664
+
'auto_discovery' => [
665
+
'enabled' => false,
666
+
],
667
+
668
+
'signals' => [
669
+
\App\Signals\NewPostSignal::class,
670
+
\App\Signals\NewFollowSignal::class,
671
+
],
672
+
```
673
+
674
+
## Testing Signals
675
+
676
+
Test your Signals before deploying:
677
+
678
+
```bash
679
+
php artisan signal:test MySignal
680
+
```
681
+
682
+
[Learn more about testing โ](testing.md)
683
+
684
+
## Listing Signals
685
+
686
+
View all registered Signals:
687
+
688
+
```bash
689
+
php artisan signal:list
690
+
```
691
+
692
+
This displays:
693
+
- Signal class names
694
+
- Event types they listen for
695
+
- Collection filters (if any)
696
+
- Queue configuration
697
+
698
+
## Next Steps
699
+
700
+
- **[Learn about filtering โ](filtering.md)** - Master collection patterns and wildcards
701
+
- **[Understand queue integration โ](queues.md)** - Build high-performance processors
702
+
- **[See real-world examples โ](examples.md)** - Learn from production use cases
+728
docs/testing.md
+728
docs/testing.md
···
1
+
# Testing Signals
2
+
3
+
Testing your Signals ensures they behave correctly before deploying to production. Signal provides tools for both manual and automated testing.
4
+
5
+
## Quick Testing with Artisan
6
+
7
+
The fastest way to test a Signal is with the `signal:test` command.
8
+
9
+
### Test a Signal
10
+
11
+
```bash
12
+
php artisan signal:test NewPostSignal
13
+
```
14
+
15
+
This runs your Signal with sample event data and displays the output.
16
+
17
+
### What It Does
18
+
19
+
1. Creates a sample `SignalEvent` matching your Signal's filters
20
+
2. Calls your Signal's `handle()` method
21
+
3. Displays output, logs, and any errors
22
+
4. Shows execution time
23
+
24
+
### Example Output
25
+
26
+
```
27
+
Testing Signal: App\Signals\NewPostSignal
28
+
29
+
Creating sample commit event for collection: app.bsky.feed.post
30
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
31
+
32
+
Event Details:
33
+
DID: did:plc:test123
34
+
Collection: app.bsky.feed.post
35
+
Operation: create
36
+
Text: Sample post for testing
37
+
38
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
39
+
40
+
Processing event...
41
+
โ Signal processed successfully
42
+
43
+
Execution time: 12ms
44
+
```
45
+
46
+
### Limitations
47
+
48
+
- Uses sample data (not real events)
49
+
- Doesn't test filtering logic comprehensively
50
+
- Can't test queue behavior
51
+
- Limited to basic scenarios
52
+
53
+
For comprehensive testing, write automated tests.
54
+
55
+
## Unit Testing
56
+
57
+
Test your Signals in isolation.
58
+
59
+
### Basic Test Structure
60
+
61
+
```php
62
+
<?php
63
+
64
+
namespace Tests\Unit\Signals;
65
+
66
+
use App\Signals\NewPostSignal;
67
+
use SocialDept\AtpSignals\Events\CommitEvent;
68
+
use SocialDept\AtpSignals\Events\SignalEvent;
69
+
use Tests\TestCase;
70
+
71
+
class NewPostSignalTest extends TestCase
72
+
{
73
+
/** @test */
74
+
public function it_handles_new_posts()
75
+
{
76
+
$signal = new NewPostSignal();
77
+
78
+
$event = new SignalEvent(
79
+
did: 'did:plc:test123',
80
+
timeUs: time() * 1000000,
81
+
kind: 'commit',
82
+
commit: new CommitEvent(
83
+
rev: 'test',
84
+
operation: 'create',
85
+
collection: 'app.bsky.feed.post',
86
+
rkey: 'test123',
87
+
record: (object) [
88
+
'text' => 'Hello World!',
89
+
'createdAt' => now()->toIso8601String(),
90
+
],
91
+
),
92
+
);
93
+
94
+
$signal->handle($event);
95
+
96
+
// Assert expected behavior
97
+
$this->assertDatabaseHas('posts', [
98
+
'text' => 'Hello World!',
99
+
]);
100
+
}
101
+
}
102
+
```
103
+
104
+
### Testing Event Types
105
+
106
+
Verify your Signal listens for correct event types:
107
+
108
+
```php
109
+
/** @test */
110
+
public function it_listens_for_commit_events()
111
+
{
112
+
$signal = new NewPostSignal();
113
+
114
+
$eventTypes = $signal->eventTypes();
115
+
116
+
$this->assertContains('commit', $eventTypes);
117
+
}
118
+
```
119
+
120
+
### Testing Filters
121
+
122
+
Verify collection filtering:
123
+
124
+
```php
125
+
/** @test */
126
+
public function it_filters_to_posts_only()
127
+
{
128
+
$signal = new NewPostSignal();
129
+
130
+
$collections = $signal->collections();
131
+
132
+
$this->assertEquals(['app.bsky.feed.post'], $collections);
133
+
}
134
+
```
135
+
136
+
### Testing Operation Filtering
137
+
138
+
```php
139
+
/** @test */
140
+
public function it_only_handles_creates()
141
+
{
142
+
$signal = new NewPostSignal();
143
+
144
+
$operations = $signal->operations();
145
+
146
+
$this->assertEquals([SignalCommitOperation::Create], $operations);
147
+
}
148
+
```
149
+
150
+
### Testing Custom Filtering
151
+
152
+
```php
153
+
/** @test */
154
+
public function it_filters_posts_with_images()
155
+
{
156
+
$signal = new ImagePostSignal();
157
+
158
+
// Event with image
159
+
$eventWithImage = $this->createEvent([
160
+
'text' => 'Check this out!',
161
+
'embed' => (object) ['type' => 'image'],
162
+
]);
163
+
164
+
$this->assertTrue($signal->shouldHandle($eventWithImage));
165
+
166
+
// Event without image
167
+
$eventWithoutImage = $this->createEvent([
168
+
'text' => 'Just text',
169
+
]);
170
+
171
+
$this->assertFalse($signal->shouldHandle($eventWithoutImage));
172
+
}
173
+
```
174
+
175
+
## Feature Testing
176
+
177
+
Test Signals in the context of your application.
178
+
179
+
### Test with Database
180
+
181
+
```php
182
+
<?php
183
+
184
+
namespace Tests\Feature\Signals;
185
+
186
+
use App\Models\Post;
187
+
use App\Signals\StorePostSignal;
188
+
use Illuminate\Foundation\Testing\RefreshDatabase;
189
+
use SocialDept\AtpSignals\Events\CommitEvent;
190
+
use SocialDept\AtpSignals\Events\SignalEvent;
191
+
use Tests\TestCase;
192
+
193
+
class StorePostSignalTest extends TestCase
194
+
{
195
+
use RefreshDatabase;
196
+
197
+
/** @test */
198
+
public function it_stores_posts_in_database()
199
+
{
200
+
$signal = new StorePostSignal();
201
+
202
+
$event = new SignalEvent(
203
+
did: 'did:plc:test123',
204
+
timeUs: time() * 1000000,
205
+
kind: 'commit',
206
+
commit: new CommitEvent(
207
+
rev: 'abc',
208
+
operation: 'create',
209
+
collection: 'app.bsky.feed.post',
210
+
rkey: 'test',
211
+
record: (object) [
212
+
'text' => 'Test post',
213
+
'createdAt' => now()->toIso8601String(),
214
+
],
215
+
),
216
+
);
217
+
218
+
$signal->handle($event);
219
+
220
+
$this->assertDatabaseHas('posts', [
221
+
'did' => 'did:plc:test123',
222
+
'text' => 'Test post',
223
+
]);
224
+
}
225
+
226
+
/** @test */
227
+
public function it_updates_existing_posts()
228
+
{
229
+
Post::create([
230
+
'did' => 'did:plc:test123',
231
+
'rkey' => 'test',
232
+
'text' => 'Old text',
233
+
]);
234
+
235
+
$signal = new StorePostSignal();
236
+
237
+
$event = $this->createUpdateEvent([
238
+
'text' => 'New text',
239
+
]);
240
+
241
+
$signal->handle($event);
242
+
243
+
$this->assertDatabaseHas('posts', [
244
+
'did' => 'did:plc:test123',
245
+
'text' => 'New text',
246
+
]);
247
+
248
+
$this->assertEquals(1, Post::count());
249
+
}
250
+
251
+
/** @test */
252
+
public function it_deletes_posts()
253
+
{
254
+
Post::create([
255
+
'did' => 'did:plc:test123',
256
+
'rkey' => 'test',
257
+
'text' => 'Test post',
258
+
]);
259
+
260
+
$signal = new StorePostSignal();
261
+
262
+
$event = $this->createDeleteEvent();
263
+
264
+
$signal->handle($event);
265
+
266
+
$this->assertDatabaseMissing('posts', [
267
+
'did' => 'did:plc:test123',
268
+
'rkey' => 'test',
269
+
]);
270
+
}
271
+
}
272
+
```
273
+
274
+
### Test with External APIs
275
+
276
+
```php
277
+
use Illuminate\Support\Facades\Http;
278
+
279
+
/** @test */
280
+
public function it_sends_notifications()
281
+
{
282
+
Http::fake();
283
+
284
+
$signal = new NotificationSignal();
285
+
286
+
$event = $this->createEvent();
287
+
288
+
$signal->handle($event);
289
+
290
+
Http::assertSent(function ($request) {
291
+
return $request->url() === 'https://api.example.com/notify' &&
292
+
$request['text'] === 'New post created';
293
+
});
294
+
}
295
+
```
296
+
297
+
## Testing Queued Signals
298
+
299
+
Test Signals that use queues.
300
+
301
+
### Test Queue Dispatch
302
+
303
+
```php
304
+
use Illuminate\Support\Facades\Queue;
305
+
306
+
/** @test */
307
+
public function it_queues_events()
308
+
{
309
+
Queue::fake();
310
+
311
+
$signal = new QueuedSignal();
312
+
313
+
$this->assertTrue($signal->shouldQueue());
314
+
315
+
// In production, this would queue
316
+
// For testing, we verify the intent
317
+
}
318
+
```
319
+
320
+
### Test with Sync Queue
321
+
322
+
Process queued jobs synchronously in tests:
323
+
324
+
```php
325
+
/** @test */
326
+
public function it_processes_queued_events()
327
+
{
328
+
// Use sync queue for immediate processing
329
+
config(['queue.default' => 'sync']);
330
+
331
+
$signal = new QueuedSignal();
332
+
333
+
$event = $this->createEvent();
334
+
335
+
$signal->handle($event);
336
+
337
+
// Assert side effects happened
338
+
$this->assertDatabaseHas('posts', [...]);
339
+
}
340
+
```
341
+
342
+
### Test Queue Configuration
343
+
344
+
```php
345
+
/** @test */
346
+
public function it_uses_high_priority_queue()
347
+
{
348
+
$signal = new HighPrioritySignal();
349
+
350
+
$this->assertTrue($signal->shouldQueue());
351
+
$this->assertEquals('high-priority', $signal->queue());
352
+
}
353
+
354
+
/** @test */
355
+
public function it_uses_redis_connection()
356
+
{
357
+
$signal = new RedisQueueSignal();
358
+
359
+
$this->assertEquals('redis', $signal->queueConnection());
360
+
}
361
+
```
362
+
363
+
## Testing Failure Handling
364
+
365
+
Test how your Signal handles errors.
366
+
367
+
### Test Failed Method
368
+
369
+
```php
370
+
use Illuminate\Support\Facades\Log;
371
+
372
+
/** @test */
373
+
public function it_logs_failures()
374
+
{
375
+
Log::spy();
376
+
377
+
$signal = new FailureHandlingSignal();
378
+
379
+
$event = $this->createEvent();
380
+
$exception = new \Exception('Something went wrong');
381
+
382
+
$signal->failed($event, $exception);
383
+
384
+
Log::shouldHaveReceived('error')
385
+
->with('Signal failed', \Mockery::any());
386
+
}
387
+
```
388
+
389
+
### Test Exception Handling
390
+
391
+
```php
392
+
/** @test */
393
+
public function it_handles_invalid_data_gracefully()
394
+
{
395
+
$signal = new RobustSignal();
396
+
397
+
$event = new SignalEvent(
398
+
did: 'did:plc:test',
399
+
timeUs: time() * 1000000,
400
+
kind: 'commit',
401
+
commit: new CommitEvent(
402
+
rev: 'test',
403
+
operation: 'create',
404
+
collection: 'app.bsky.feed.post',
405
+
rkey: 'test',
406
+
record: (object) [], // Missing required fields
407
+
),
408
+
);
409
+
410
+
// Should not throw
411
+
$signal->handle($event);
412
+
413
+
// Should handle gracefully (e.g., log and skip)
414
+
$this->assertDatabaseCount('posts', 0);
415
+
}
416
+
```
417
+
418
+
## Test Helpers
419
+
420
+
Create reusable helpers for common test scenarios.
421
+
422
+
### Event Factory Helper
423
+
424
+
```php
425
+
trait CreatesSignalEvents
426
+
{
427
+
protected function createCommitEvent(array $overrides = []): SignalEvent
428
+
{
429
+
$defaults = [
430
+
'did' => 'did:plc:test123',
431
+
'timeUs' => time() * 1000000,
432
+
'kind' => 'commit',
433
+
'commit' => new CommitEvent(
434
+
rev: 'test',
435
+
operation: $overrides['operation'] ?? 'create',
436
+
collection: $overrides['collection'] ?? 'app.bsky.feed.post',
437
+
rkey: $overrides['rkey'] ?? 'test',
438
+
record: (object) array_merge([
439
+
'text' => 'Test post',
440
+
'createdAt' => now()->toIso8601String(),
441
+
], $overrides['record'] ?? []),
442
+
),
443
+
];
444
+
445
+
return new SignalEvent(...$defaults);
446
+
}
447
+
448
+
protected function createPostEvent(array $record = []): SignalEvent
449
+
{
450
+
return $this->createCommitEvent([
451
+
'collection' => 'app.bsky.feed.post',
452
+
'record' => $record,
453
+
]);
454
+
}
455
+
456
+
protected function createLikeEvent(array $record = []): SignalEvent
457
+
{
458
+
return $this->createCommitEvent([
459
+
'collection' => 'app.bsky.feed.like',
460
+
'record' => array_merge([
461
+
'subject' => (object) [
462
+
'uri' => 'at://did:plc:test/app.bsky.feed.post/test',
463
+
'cid' => 'bafytest',
464
+
],
465
+
'createdAt' => now()->toIso8601String(),
466
+
], $record),
467
+
]);
468
+
}
469
+
470
+
protected function createFollowEvent(array $record = []): SignalEvent
471
+
{
472
+
return $this->createCommitEvent([
473
+
'collection' => 'app.bsky.graph.follow',
474
+
'record' => array_merge([
475
+
'subject' => 'did:plc:target',
476
+
'createdAt' => now()->toIso8601String(),
477
+
], $record),
478
+
]);
479
+
}
480
+
}
481
+
```
482
+
483
+
Use in tests:
484
+
485
+
```php
486
+
class MySignalTest extends TestCase
487
+
{
488
+
use CreatesSignalEvents;
489
+
490
+
/** @test */
491
+
public function it_handles_posts()
492
+
{
493
+
$event = $this->createPostEvent([
494
+
'text' => 'Custom text',
495
+
]);
496
+
497
+
// Test with event
498
+
}
499
+
}
500
+
```
501
+
502
+
### Signal Factory Helper
503
+
504
+
```php
505
+
trait CreatesSignals
506
+
{
507
+
protected function createSignal(string $class, array $config = [])
508
+
{
509
+
$signal = new $class();
510
+
511
+
// Override configuration for testing
512
+
foreach ($config as $method => $value) {
513
+
$signal->{$method} = $value;
514
+
}
515
+
516
+
return $signal;
517
+
}
518
+
}
519
+
```
520
+
521
+
## Testing Best Practices
522
+
523
+
### Use Descriptive Test Names
524
+
525
+
```php
526
+
// Good
527
+
/** @test */
528
+
public function it_stores_posts_with_valid_data()
529
+
530
+
/** @test */
531
+
public function it_skips_posts_without_text()
532
+
533
+
/** @test */
534
+
public function it_handles_duplicate_posts_gracefully()
535
+
536
+
// Less descriptive
537
+
/** @test */
538
+
public function test_handle()
539
+
```
540
+
541
+
### Test Edge Cases
542
+
543
+
```php
544
+
/** @test */
545
+
public function it_handles_empty_text()
546
+
{
547
+
$event = $this->createPostEvent(['text' => '']);
548
+
// Test behavior
549
+
}
550
+
551
+
/** @test */
552
+
public function it_handles_very_long_text()
553
+
{
554
+
$event = $this->createPostEvent(['text' => str_repeat('a', 10000)]);
555
+
// Test behavior
556
+
}
557
+
558
+
/** @test */
559
+
public function it_handles_missing_created_at()
560
+
{
561
+
$event = $this->createPostEvent(['createdAt' => null]);
562
+
// Test behavior
563
+
}
564
+
```
565
+
566
+
### Test All Operations
567
+
568
+
```php
569
+
/** @test */
570
+
public function it_handles_creates()
571
+
{
572
+
$event = $this->createEvent(['operation' => 'create']);
573
+
// Test
574
+
}
575
+
576
+
/** @test */
577
+
public function it_handles_updates()
578
+
{
579
+
$event = $this->createEvent(['operation' => 'update']);
580
+
// Test
581
+
}
582
+
583
+
/** @test */
584
+
public function it_handles_deletes()
585
+
{
586
+
$event = $this->createEvent(['operation' => 'delete']);
587
+
// Test
588
+
}
589
+
```
590
+
591
+
### Mock External Dependencies
592
+
593
+
```php
594
+
/** @test */
595
+
public function it_calls_external_api()
596
+
{
597
+
Http::fake([
598
+
'api.example.com/*' => Http::response(['success' => true]),
599
+
]);
600
+
601
+
$signal = new ApiSignal();
602
+
$event = $this->createEvent();
603
+
604
+
$signal->handle($event);
605
+
606
+
Http::assertSent(function ($request) {
607
+
return $request->url() === 'https://api.example.com/endpoint';
608
+
});
609
+
}
610
+
```
611
+
612
+
### Test Database State
613
+
614
+
```php
615
+
use Illuminate\Foundation\Testing\RefreshDatabase;
616
+
617
+
class DatabaseSignalTest extends TestCase
618
+
{
619
+
use RefreshDatabase;
620
+
621
+
/** @test */
622
+
public function it_creates_records()
623
+
{
624
+
// Fresh database for each test
625
+
}
626
+
}
627
+
```
628
+
629
+
## Continuous Integration
630
+
631
+
Run tests automatically on every commit.
632
+
633
+
### GitHub Actions
634
+
635
+
```yaml
636
+
# .github/workflows/tests.yml
637
+
name: Tests
638
+
639
+
on: [push, pull_request]
640
+
641
+
jobs:
642
+
test:
643
+
runs-on: ubuntu-latest
644
+
645
+
steps:
646
+
- uses: actions/checkout@v2
647
+
648
+
- name: Setup PHP
649
+
uses: shivammathur/setup-php@v2
650
+
with:
651
+
php-version: 8.2
652
+
653
+
- name: Install Dependencies
654
+
run: composer install
655
+
656
+
- name: Run Tests
657
+
run: php artisan test
658
+
```
659
+
660
+
### Run Signal-Specific Tests
661
+
662
+
```bash
663
+
# Run all Signal tests
664
+
php artisan test --testsuite=Signals
665
+
666
+
# Run specific test file
667
+
php artisan test tests/Unit/Signals/NewPostSignalTest.php
668
+
669
+
# Run with coverage
670
+
php artisan test --coverage
671
+
```
672
+
673
+
## Debugging Tests
674
+
675
+
### Enable Debug Output
676
+
677
+
```php
678
+
/** @test */
679
+
public function it_processes_events()
680
+
{
681
+
$signal = new NewPostSignal();
682
+
683
+
$event = $this->createEvent();
684
+
685
+
dump($event); // Output event data
686
+
687
+
$signal->handle($event);
688
+
689
+
dump(Post::all()); // Output results
690
+
}
691
+
```
692
+
693
+
### Use dd() to Stop Execution
694
+
695
+
```php
696
+
/** @test */
697
+
public function it_processes_events()
698
+
{
699
+
$event = $this->createEvent();
700
+
701
+
dd($event); // Dump and die
702
+
703
+
// This won't run
704
+
}
705
+
```
706
+
707
+
### Check Logs
708
+
709
+
```php
710
+
/** @test */
711
+
public function it_logs_processing()
712
+
{
713
+
Log::spy();
714
+
715
+
$signal = new LoggingSignal();
716
+
$event = $this->createEvent();
717
+
718
+
$signal->handle($event);
719
+
720
+
Log::shouldHaveReceived('info')->once();
721
+
}
722
+
```
723
+
724
+
## Next Steps
725
+
726
+
- **[See real-world examples โ](examples.md)** - Learn from production test patterns
727
+
- **[Review queue integration โ](queues.md)** - Test queued Signals
728
+
- **[Review signals documentation โ](signals.md)** - Understand Signal structure
header.png
header.png
This is a binary file and will not be displayed.
+4
-3
src/Binary/Reader.php
+4
-3
src/Binary/Reader.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\Binary;
5
+
namespace SocialDept\AtpSignals\Binary;
6
6
7
7
use RuntimeException;
8
8
···
17
17
18
18
public function __construct(
19
19
private readonly string $data,
20
-
) {}
20
+
) {
21
+
}
21
22
22
23
/**
23
24
* Get current position in the data.
···
58
59
*/
59
60
public function peek(): int
60
61
{
61
-
if (!$this->hasMore()) {
62
+
if (! $this->hasMore()) {
62
63
throw new RuntimeException('Unexpected end of data');
63
64
}
64
65
+1
-1
src/Binary/Varint.php
+1
-1
src/Binary/Varint.php
+5
-6
src/CAR/BlockReader.php
+5
-6
src/CAR/BlockReader.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\CAR;
5
+
namespace SocialDept\AtpSignals\CAR;
6
6
7
7
use Generator;
8
-
use SocialDept\Signal\Binary\Reader;
9
-
use SocialDept\Signal\Core\CID;
10
-
use SocialDept\Signal\Core\CBOR;
8
+
use SocialDept\AtpSignals\Binary\Reader;
9
+
use SocialDept\AtpSignals\Core\CID;
11
10
12
11
/**
13
12
* CAR (Content Addressable aRchive) block reader.
···
49
48
*/
50
49
private function skipHeader(): void
51
50
{
52
-
if (!$this->reader->hasMore()) {
51
+
if (! $this->reader->hasMore()) {
53
52
return;
54
53
}
55
54
···
67
66
*/
68
67
private function readBlock(): ?array
69
68
{
70
-
if (!$this->reader->hasMore()) {
69
+
if (! $this->reader->hasMore()) {
71
70
return null;
72
71
}
73
72
+9
-8
src/CAR/RecordExtractor.php
+9
-8
src/CAR/RecordExtractor.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\CAR;
5
+
namespace SocialDept\AtpSignals\CAR;
6
6
7
7
use Generator;
8
-
use SocialDept\Signal\Core\CID;
9
-
use SocialDept\Signal\Core\CBOR;
8
+
use SocialDept\AtpSignals\Core\CBOR;
9
+
use SocialDept\AtpSignals\Core\CID;
10
10
11
11
/**
12
12
* Extract records from AT Protocol MST (Merkle Search Tree) blocks.
···
21
21
public function __construct(
22
22
private readonly array $blocks,
23
23
private readonly string $did,
24
-
) {}
24
+
) {
25
+
}
25
26
26
27
/**
27
28
* Extract all records from blocks.
···
47
48
$cidStr = $cid->toString();
48
49
49
50
// Get block data
50
-
if (!isset($this->blocks[$cidStr])) {
51
+
if (! isset($this->blocks[$cidStr])) {
51
52
// Block not found - might be a pruned tree, skip it
52
53
return;
53
54
}
···
57
58
// Decode CBOR block
58
59
$node = CBOR::decode($blockData);
59
60
60
-
if (!is_array($node)) {
61
+
if (! is_array($node)) {
61
62
return;
62
63
}
63
64
···
69
70
// Process entries
70
71
if (isset($node['e']) && is_array($node['e'])) {
71
72
foreach ($node['e'] as $entry) {
72
-
if (!is_array($entry)) {
73
+
if (! is_array($entry)) {
73
74
continue;
74
75
}
75
76
···
121
122
{
122
123
$cidStr = $cid->toString();
123
124
124
-
if (!isset($this->blocks[$cidStr])) {
125
+
if (! isset($this->blocks[$cidStr])) {
125
126
return null;
126
127
}
127
128
+3
-3
src/CBOR/Decoder.php
+3
-3
src/CBOR/Decoder.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\CBOR;
5
+
namespace SocialDept\AtpSignals\CBOR;
6
6
7
7
use RuntimeException;
8
-
use SocialDept\Signal\Binary\Reader;
9
-
use SocialDept\Signal\Core\CID;
8
+
use SocialDept\AtpSignals\Binary\Reader;
9
+
use SocialDept\AtpSignals\Core\CID;
10
10
11
11
/**
12
12
* CBOR (Concise Binary Object Representation) decoder.
+4
-4
src/Commands/ConsumeCommand.php
+4
-4
src/Commands/ConsumeCommand.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Commands;
3
+
namespace SocialDept\AtpSignals\Commands;
4
4
5
5
use BackedEnum;
6
6
use Exception;
7
7
use Illuminate\Console\Command;
8
-
use SocialDept\Signal\Services\FirehoseConsumer;
9
-
use SocialDept\Signal\Services\JetstreamConsumer;
10
-
use SocialDept\Signal\Services\SignalRegistry;
8
+
use SocialDept\AtpSignals\Services\FirehoseConsumer;
9
+
use SocialDept\AtpSignals\Services\JetstreamConsumer;
10
+
use SocialDept\AtpSignals\Services\SignalRegistry;
11
11
12
12
class ConsumeCommand extends Command
13
13
{
+1
-1
src/Commands/InstallCommand.php
+1
-1
src/Commands/InstallCommand.php
+2
-2
src/Commands/ListSignalsCommand.php
+2
-2
src/Commands/ListSignalsCommand.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Commands;
3
+
namespace SocialDept\AtpSignals\Commands;
4
4
5
5
use Illuminate\Console\Command;
6
6
use Illuminate\Support\Collection;
7
-
use SocialDept\Signal\Services\SignalRegistry;
7
+
use SocialDept\AtpSignals\Services\SignalRegistry;
8
8
9
9
class ListSignalsCommand extends Command
10
10
{
+1
-1
src/Commands/MakeSignalCommand.php
+1
-1
src/Commands/MakeSignalCommand.php
+3
-3
src/Commands/TestSignalCommand.php
+3
-3
src/Commands/TestSignalCommand.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Commands;
3
+
namespace SocialDept\AtpSignals\Commands;
4
4
5
5
use Illuminate\Console\Command;
6
6
use InvalidArgumentException;
7
-
use SocialDept\Signal\Events\CommitEvent;
8
-
use SocialDept\Signal\Events\SignalEvent;
7
+
use SocialDept\AtpSignals\Events\CommitEvent;
8
+
use SocialDept\AtpSignals\Events\SignalEvent;
9
9
10
10
class TestSignalCommand extends Command
11
11
{
+1
-1
src/Contracts/CursorStore.php
+1
-1
src/Contracts/CursorStore.php
+1
-1
src/Contracts/EventContract.php
+1
-1
src/Contracts/EventContract.php
+5
-6
src/Core/CAR.php
+5
-6
src/Core/CAR.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\Core;
5
+
namespace SocialDept\AtpSignals\Core;
6
6
7
-
use Generator;
8
-
use SocialDept\Signal\CAR\BlockReader;
9
-
use SocialDept\Signal\CAR\RecordExtractor;
7
+
use SocialDept\AtpSignals\CAR\BlockReader;
10
8
11
9
/**
12
10
* CAR (Content Addressable aRchive) facade.
···
29
27
{
30
28
// Read all blocks from CAR
31
29
$blockReader = new BlockReader($data);
30
+
32
31
return $blockReader->getBlockMap();
33
32
}
34
33
···
46
45
47
46
$decoded = CBOR::decode($firstBlock);
48
47
49
-
if (!is_array($decoded)) {
48
+
if (! is_array($decoded)) {
50
49
return null;
51
50
}
52
51
···
67
66
68
67
$commit = CBOR::decode($firstBlock);
69
68
70
-
if (!is_array($commit)) {
69
+
if (! is_array($commit)) {
71
70
return null;
72
71
}
73
72
+2
-2
src/Core/CBOR.php
+2
-2
src/Core/CBOR.php
+5
-4
src/Core/CID.php
+5
-4
src/Core/CID.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\Core;
5
+
namespace SocialDept\AtpSignals\Core;
6
6
7
7
use RuntimeException;
8
-
use SocialDept\Signal\Binary\Reader;
9
-
use SocialDept\Signal\Binary\Varint;
8
+
use SocialDept\AtpSignals\Binary\Reader;
9
+
use SocialDept\AtpSignals\Binary\Varint;
10
10
11
11
/**
12
12
* Content Identifier (CID) parser for IPLD.
···
23
23
public readonly int $version,
24
24
public readonly int $codec,
25
25
public readonly string $hash,
26
-
) {}
26
+
) {
27
+
}
27
28
28
29
/**
29
30
* Parse CID from binary data.
+1
-1
src/Enums/SignalCommitOperation.php
+1
-1
src/Enums/SignalCommitOperation.php
+1
-1
src/Enums/SignalEventType.php
+1
-1
src/Enums/SignalEventType.php
+4
-3
src/Events/AccountEvent.php
+4
-3
src/Events/AccountEvent.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Events;
3
+
namespace SocialDept\AtpSignals\Events;
4
4
5
-
use SocialDept\Signal\Contracts\EventContract;
5
+
use SocialDept\AtpSignals\Contracts\EventContract;
6
6
7
7
class AccountEvent implements EventContract
8
8
{
···
12
12
public ?string $status = null,
13
13
public int $seq = 0,
14
14
public ?string $time = null,
15
-
) {}
15
+
) {
16
+
}
16
17
17
18
public static function fromArray(array $data): self
18
19
{
+3
-3
src/Events/CommitEvent.php
+3
-3
src/Events/CommitEvent.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Events;
3
+
namespace SocialDept\AtpSignals\Events;
4
4
5
-
use SocialDept\Signal\Contracts\EventContract;
6
-
use SocialDept\Signal\Enums\SignalCommitOperation;
5
+
use SocialDept\AtpSignals\Contracts\EventContract;
6
+
use SocialDept\AtpSignals\Enums\SignalCommitOperation;
7
7
8
8
class CommitEvent implements EventContract
9
9
{
+4
-3
src/Events/IdentityEvent.php
+4
-3
src/Events/IdentityEvent.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Events;
3
+
namespace SocialDept\AtpSignals\Events;
4
4
5
-
use SocialDept\Signal\Contracts\EventContract;
5
+
use SocialDept\AtpSignals\Contracts\EventContract;
6
6
7
7
class IdentityEvent implements EventContract
8
8
{
···
11
11
public ?string $handle = null,
12
12
public int $seq = 0,
13
13
public ?string $time = null,
14
-
) {}
14
+
) {
15
+
}
15
16
16
17
public static function fromArray(array $data): self
17
18
{
+5
-4
src/Events/SignalEvent.php
+5
-4
src/Events/SignalEvent.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Events;
3
+
namespace SocialDept\AtpSignals\Events;
4
4
5
-
use SocialDept\Signal\Contracts\EventContract;
5
+
use SocialDept\AtpSignals\Contracts\EventContract;
6
6
7
7
class SignalEvent implements EventContract
8
8
{
···
13
13
public ?CommitEvent $commit = null,
14
14
public ?IdentityEvent $identity = null,
15
15
public ?AccountEvent $account = null,
16
-
) {}
16
+
) {
17
+
}
17
18
18
19
public function isCommit(): bool
19
20
{
···
40
41
return $this->commit?->record;
41
42
}
42
43
43
-
public function getOperation(): ?\SocialDept\Signal\Enums\SignalCommitOperation
44
+
public function getOperation(): ?\SocialDept\AtpSignals\Enums\SignalCommitOperation
44
45
{
45
46
return $this->commit?->operation;
46
47
}
+1
-1
src/Exceptions/ConnectionException.php
+1
-1
src/Exceptions/ConnectionException.php
+1
-1
src/Exceptions/SignalException.php
+1
-1
src/Exceptions/SignalException.php
+3
-3
src/Facades/Signal.php
+3
-3
src/Facades/Signal.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Facades;
3
+
namespace SocialDept\AtpSignals\Facades;
4
4
5
5
use Illuminate\Support\Facades\Facade;
6
-
use SocialDept\Signal\Services\SignalManager;
6
+
use SocialDept\AtpSignals\Services\SignalManager;
7
7
8
8
/**
9
9
* @method static void start(?int $cursor = null)
10
10
* @method static void stop()
11
11
* @method static string getMode()
12
12
*
13
-
* @see \SocialDept\Signal\Services\SignalManager
13
+
* @see \SocialDept\AtpSignals\Services\SignalManager
14
14
*/
15
15
class Signal extends Facade
16
16
{
+9
-5
src/Jobs/ProcessSignalJob.php
+9
-5
src/Jobs/ProcessSignalJob.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Jobs;
3
+
namespace SocialDept\AtpSignals\Jobs;
4
4
5
5
use Illuminate\Bus\Queueable;
6
6
use Illuminate\Contracts\Queue\ShouldQueue;
7
7
use Illuminate\Foundation\Bus\Dispatchable;
8
8
use Illuminate\Queue\InteractsWithQueue;
9
9
use Illuminate\Queue\SerializesModels;
10
-
use SocialDept\Signal\Events\SignalEvent;
11
-
use SocialDept\Signal\Signals\Signal;
10
+
use SocialDept\AtpSignals\Events\SignalEvent;
11
+
use SocialDept\AtpSignals\Signals\Signal;
12
12
13
13
class ProcessSignalJob implements ShouldQueue
14
14
{
15
-
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
15
+
use Dispatchable;
16
+
use InteractsWithQueue;
17
+
use Queueable;
18
+
use SerializesModels;
16
19
17
20
public function __construct(
18
21
protected Signal $signal,
19
22
protected SignalEvent $event,
20
-
) {}
23
+
) {
24
+
}
21
25
22
26
public function handle(): void
23
27
{
+3
-3
src/Services/EventDispatcher.php
+3
-3
src/Services/EventDispatcher.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Services;
3
+
namespace SocialDept\AtpSignals\Services;
4
4
5
5
use Illuminate\Support\Facades\Log;
6
6
use Illuminate\Support\Facades\Queue;
7
-
use SocialDept\Signal\Events\SignalEvent;
8
-
use SocialDept\Signal\Jobs\ProcessSignalJob;
7
+
use SocialDept\AtpSignals\Events\SignalEvent;
8
+
use SocialDept\AtpSignals\Jobs\ProcessSignalJob;
9
9
10
10
class EventDispatcher
11
11
{
+13
-12
src/Services/FirehoseConsumer.php
+13
-12
src/Services/FirehoseConsumer.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Services;
3
+
namespace SocialDept\AtpSignals\Services;
4
4
5
5
use Illuminate\Support\Arr;
6
6
use Illuminate\Support\Facades\Log;
7
-
use SocialDept\Signal\Contracts\CursorStore;
8
-
use SocialDept\Signal\Core\CAR;
9
-
use SocialDept\Signal\Core\CBOR;
10
-
use SocialDept\Signal\Core\CID;
11
-
use SocialDept\Signal\Events\AccountEvent;
12
-
use SocialDept\Signal\Events\CommitEvent;
13
-
use SocialDept\Signal\Events\IdentityEvent;
14
-
use SocialDept\Signal\Events\SignalEvent;
15
-
use SocialDept\Signal\Exceptions\ConnectionException;
16
-
use SocialDept\Signal\Support\WebSocketConnection;
7
+
use SocialDept\AtpSignals\Contracts\CursorStore;
8
+
use SocialDept\AtpSignals\Core\CAR;
9
+
use SocialDept\AtpSignals\Core\CBOR;
10
+
use SocialDept\AtpSignals\Core\CID;
11
+
use SocialDept\AtpSignals\Events\AccountEvent;
12
+
use SocialDept\AtpSignals\Events\CommitEvent;
13
+
use SocialDept\AtpSignals\Events\IdentityEvent;
14
+
use SocialDept\AtpSignals\Events\SignalEvent;
15
+
use SocialDept\AtpSignals\Exceptions\ConnectionException;
16
+
use SocialDept\AtpSignals\Support\WebSocketConnection;
17
17
18
18
class FirehoseConsumer
19
19
{
···
83
83
*/
84
84
protected function connect(string $url): void
85
85
{
86
-
$this->connection = new WebSocketConnection;
86
+
$this->connection = new WebSocketConnection();
87
87
88
88
// Set up event handlers
89
89
$this->connection
···
413
413
414
414
if ($this->reconnectAttempts >= $maxAttempts) {
415
415
Log::error('Signal: Max reconnection attempts reached');
416
+
416
417
throw new ConnectionException('Failed to reconnect to Firehose after '.$maxAttempts.' attempts');
417
418
}
418
419
+7
-6
src/Services/JetstreamConsumer.php
+7
-6
src/Services/JetstreamConsumer.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Services;
3
+
namespace SocialDept\AtpSignals\Services;
4
4
5
5
use Illuminate\Support\Facades\Log;
6
-
use SocialDept\Signal\Contracts\CursorStore;
7
-
use SocialDept\Signal\Events\SignalEvent;
8
-
use SocialDept\Signal\Exceptions\ConnectionException;
9
-
use SocialDept\Signal\Support\WebSocketConnection;
6
+
use SocialDept\AtpSignals\Contracts\CursorStore;
7
+
use SocialDept\AtpSignals\Events\SignalEvent;
8
+
use SocialDept\AtpSignals\Exceptions\ConnectionException;
9
+
use SocialDept\AtpSignals\Support\WebSocketConnection;
10
10
11
11
class JetstreamConsumer
12
12
{
···
76
76
*/
77
77
protected function connect(string $url): void
78
78
{
79
-
$this->connection = new WebSocketConnection;
79
+
$this->connection = new WebSocketConnection();
80
80
81
81
// Set up event handlers
82
82
$this->connection
···
178
178
179
179
if ($this->reconnectAttempts >= $maxAttempts) {
180
180
Log::error('Signal: Max reconnection attempts reached');
181
+
181
182
throw new ConnectionException('Failed to reconnect to Jetstream after '.$maxAttempts.' attempts');
182
183
}
183
184
+3
-2
src/Services/SignalManager.php
+3
-2
src/Services/SignalManager.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Services;
3
+
namespace SocialDept\AtpSignals\Services;
4
4
5
5
use InvalidArgumentException;
6
6
···
9
9
public function __construct(
10
10
protected FirehoseConsumer $firehoseConsumer,
11
11
protected JetstreamConsumer $jetstreamConsumer,
12
-
) {}
12
+
) {
13
+
}
13
14
14
15
/**
15
16
* Start consuming events from the AT Protocol.
+2
-2
src/Services/SignalRegistry.php
+2
-2
src/Services/SignalRegistry.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Services;
3
+
namespace SocialDept\AtpSignals\Services;
4
4
5
5
use Illuminate\Support\Collection;
6
6
use Illuminate\Support\Facades\File;
7
7
use InvalidArgumentException;
8
-
use SocialDept\Signal\Signals\Signal;
8
+
use SocialDept\AtpSignals\Signals\Signal;
9
9
10
10
class SignalRegistry
11
11
{
+19
-19
src/SignalServiceProvider.php
+19
-19
src/SignalServiceProvider.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal;
3
+
namespace SocialDept\AtpSignals;
4
4
5
5
use Illuminate\Support\ServiceProvider;
6
-
use SocialDept\Signal\Commands\ConsumeCommand;
7
-
use SocialDept\Signal\Commands\InstallCommand;
8
-
use SocialDept\Signal\Commands\ListSignalsCommand;
9
-
use SocialDept\Signal\Commands\MakeSignalCommand;
10
-
use SocialDept\Signal\Commands\TestSignalCommand;
11
-
use SocialDept\Signal\Contracts\CursorStore;
12
-
use SocialDept\Signal\Services\EventDispatcher;
13
-
use SocialDept\Signal\Services\FirehoseConsumer;
14
-
use SocialDept\Signal\Services\JetstreamConsumer;
15
-
use SocialDept\Signal\Services\SignalManager;
16
-
use SocialDept\Signal\Services\SignalRegistry;
17
-
use SocialDept\Signal\Storage\DatabaseCursorStore;
18
-
use SocialDept\Signal\Storage\FileCursorStore;
19
-
use SocialDept\Signal\Storage\RedisCursorStore;
6
+
use SocialDept\AtpSignals\Commands\ConsumeCommand;
7
+
use SocialDept\AtpSignals\Commands\InstallCommand;
8
+
use SocialDept\AtpSignals\Commands\ListSignalsCommand;
9
+
use SocialDept\AtpSignals\Commands\MakeSignalCommand;
10
+
use SocialDept\AtpSignals\Commands\TestSignalCommand;
11
+
use SocialDept\AtpSignals\Contracts\CursorStore;
12
+
use SocialDept\AtpSignals\Services\EventDispatcher;
13
+
use SocialDept\AtpSignals\Services\FirehoseConsumer;
14
+
use SocialDept\AtpSignals\Services\JetstreamConsumer;
15
+
use SocialDept\AtpSignals\Services\SignalManager;
16
+
use SocialDept\AtpSignals\Services\SignalRegistry;
17
+
use SocialDept\AtpSignals\Storage\DatabaseCursorStore;
18
+
use SocialDept\AtpSignals\Storage\FileCursorStore;
19
+
use SocialDept\AtpSignals\Storage\RedisCursorStore;
20
20
21
21
class SignalServiceProvider extends ServiceProvider
22
22
{
···
27
27
// Register cursor store
28
28
$this->app->singleton(CursorStore::class, function ($app) {
29
29
return match (config('signal.cursor_storage')) {
30
-
'redis' => new RedisCursorStore,
31
-
'file' => new FileCursorStore,
32
-
default => new DatabaseCursorStore,
30
+
'redis' => new RedisCursorStore(),
31
+
'file' => new FileCursorStore(),
32
+
default => new DatabaseCursorStore(),
33
33
};
34
34
});
35
35
36
36
// Register signal registry
37
37
$this->app->singleton(SignalRegistry::class, function ($app) {
38
-
$registry = new SignalRegistry;
38
+
$registry = new SignalRegistry();
39
39
40
40
// Register configured signals
41
41
foreach (config('signal.signals', []) as $signal) {
+4
-4
src/Signals/Signal.php
+4
-4
src/Signals/Signal.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Signals;
3
+
namespace SocialDept\AtpSignals\Signals;
4
4
5
-
use SocialDept\Signal\Events\SignalEvent;
5
+
use SocialDept\AtpSignals\Events\SignalEvent;
6
6
7
7
abstract class Signal
8
8
{
9
9
/**
10
10
* Define which event types to listen for.
11
11
*
12
-
* @return array<string|\SocialDept\Signal\Enums\SignalEventType>
12
+
* @return array<string|\SocialDept\AtpSignals\Enums\SignalEventType>
13
13
*/
14
14
abstract public function eventTypes(): array;
15
15
···
42
42
* - [SignalCommitOperation::Delete] - Only handle deletes
43
43
* - null - Handle all operations (default)
44
44
*
45
-
* @return array<string|\SocialDept\Signal\Enums\SignalCommitOperation>|null
45
+
* @return array<string|\SocialDept\AtpSignals\Enums\SignalCommitOperation>|null
46
46
*/
47
47
public function operations(): ?array
48
48
{
+2
-2
src/Storage/DatabaseCursorStore.php
+2
-2
src/Storage/DatabaseCursorStore.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Storage;
3
+
namespace SocialDept\AtpSignals\Storage;
4
4
5
5
use Illuminate\Database\Query\Builder;
6
6
use Illuminate\Support\Facades\DB;
7
-
use SocialDept\Signal\Contracts\CursorStore;
7
+
use SocialDept\AtpSignals\Contracts\CursorStore;
8
8
9
9
class DatabaseCursorStore implements CursorStore
10
10
{
+4
-4
src/Storage/FileCursorStore.php
+4
-4
src/Storage/FileCursorStore.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Storage;
3
+
namespace SocialDept\AtpSignals\Storage;
4
4
5
5
use Illuminate\Support\Facades\File;
6
-
use SocialDept\Signal\Contracts\CursorStore;
6
+
use SocialDept\AtpSignals\Contracts\CursorStore;
7
7
8
8
class FileCursorStore implements CursorStore
9
9
{
···
15
15
16
16
// Ensure directory exists
17
17
$directory = dirname($this->path);
18
-
if (!File::exists($directory)) {
18
+
if (! File::exists($directory)) {
19
19
File::makeDirectory($directory, 0755, true);
20
20
}
21
21
}
22
22
23
23
public function get(): ?int
24
24
{
25
-
if (!File::exists($this->path)) {
25
+
if (! File::exists($this->path)) {
26
26
return null;
27
27
}
28
28
+2
-2
src/Storage/RedisCursorStore.php
+2
-2
src/Storage/RedisCursorStore.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Storage;
3
+
namespace SocialDept\AtpSignals\Storage;
4
4
5
5
use Illuminate\Support\Facades\Redis;
6
-
use SocialDept\Signal\Contracts\CursorStore;
6
+
use SocialDept\AtpSignals\Contracts\CursorStore;
7
7
8
8
class RedisCursorStore implements CursorStore
9
9
{
+8
-2
src/Support/WebSocketConnection.php
+8
-2
src/Support/WebSocketConnection.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Support;
3
+
namespace SocialDept\AtpSignals\Support;
4
4
5
5
use Ratchet\Client\Connector;
6
6
use Ratchet\Client\WebSocket;
···
64
64
if ($this->onError) {
65
65
($this->onError)($e);
66
66
}
67
+
67
68
throw $e;
68
69
}
69
70
);
···
74
75
*/
75
76
public function send(string $message): bool
76
77
{
77
-
if (!$this->connected || !$this->connection) {
78
+
if (! $this->connected || ! $this->connection) {
78
79
return false;
79
80
}
80
81
81
82
try {
82
83
$this->connection->send($message);
84
+
83
85
return true;
84
86
} catch (\Exception $e) {
85
87
if ($this->onError) {
86
88
($this->onError)($e);
87
89
}
90
+
88
91
return false;
89
92
}
90
93
}
···
114
117
public function onMessage(callable $callback): self
115
118
{
116
119
$this->onMessage = $callback(...);
120
+
117
121
return $this;
118
122
}
119
123
···
123
127
public function onClose(callable $callback): self
124
128
{
125
129
$this->onClose = $callback(...);
130
+
126
131
return $this;
127
132
}
128
133
···
132
137
public function onError(callable $callback): self
133
138
{
134
139
$this->onError = $callback(...);
140
+
135
141
return $this;
136
142
}
137
143
+2
-2
stubs/signal.stub
+2
-2
stubs/signal.stub
+4
-4
tests/Integration/FirehoseConsumerTest.php
+4
-4
tests/Integration/FirehoseConsumerTest.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\Tests\Integration;
5
+
namespace SocialDept\AtpSignals\Tests\Integration;
6
6
7
7
use Orchestra\Testbench\TestCase;
8
-
use SocialDept\Signal\Core\CAR;
9
-
use SocialDept\Signal\Core\CBOR;
10
-
use SocialDept\Signal\Core\CID;
8
+
use SocialDept\AtpSignals\Core\CAR;
9
+
use SocialDept\AtpSignals\Core\CBOR;
10
+
use SocialDept\AtpSignals\Core\CID;
11
11
12
12
class FirehoseConsumerTest extends TestCase
13
13
{
+3
-3
tests/Unit/CBORTest.php
+3
-3
tests/Unit/CBORTest.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\Tests\Unit;
5
+
namespace SocialDept\AtpSignals\Tests\Unit;
6
6
7
7
use PHPUnit\Framework\TestCase;
8
-
use SocialDept\Signal\Core\CBOR;
9
-
use SocialDept\Signal\Core\CID;
8
+
use SocialDept\AtpSignals\Core\CBOR;
9
+
use SocialDept\AtpSignals\Core\CID;
10
10
11
11
class CBORTest extends TestCase
12
12
{
+2
-2
tests/Unit/CIDTest.php
+2
-2
tests/Unit/CIDTest.php
+4
-5
tests/Unit/SignalRegistryTest.php
+4
-5
tests/Unit/SignalRegistryTest.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Tests\Unit;
3
+
namespace SocialDept\AtpSignals\Tests\Unit;
4
4
5
5
use Orchestra\Testbench\TestCase;
6
-
use SocialDept\Signal\Events\CommitEvent;
7
-
use SocialDept\Signal\Events\SignalEvent;
8
-
use SocialDept\Signal\Services\SignalRegistry;
9
-
use SocialDept\Signal\Signals\Signal;
6
+
use SocialDept\AtpSignals\Events\CommitEvent;
7
+
use SocialDept\AtpSignals\Events\SignalEvent;
8
+
use SocialDept\AtpSignals\Services\SignalRegistry;
10
9
11
10
class SignalRegistryTest extends TestCase
12
11
{
+8
-8
tests/Unit/SignalTest.php
+8
-8
tests/Unit/SignalTest.php
···
1
1
<?php
2
2
3
-
namespace SocialDept\Signal\Tests\Unit;
3
+
namespace SocialDept\AtpSignals\Tests\Unit;
4
4
5
5
use Orchestra\Testbench\TestCase;
6
-
use SocialDept\Signal\Events\CommitEvent;
7
-
use SocialDept\Signal\Events\SignalEvent;
8
-
use SocialDept\Signal\Signals\Signal;
6
+
use SocialDept\AtpSignals\Events\CommitEvent;
7
+
use SocialDept\AtpSignals\Events\SignalEvent;
8
+
use SocialDept\AtpSignals\Signals\Signal;
9
9
10
10
class SignalTest extends TestCase
11
11
{
12
12
/** @test */
13
13
public function it_can_create_a_signal()
14
14
{
15
-
$signal = new class extends Signal {
15
+
$signal = new class () extends Signal {
16
16
public function eventTypes(): array
17
17
{
18
18
return ['commit'];
···
31
31
/** @test */
32
32
public function it_can_filter_by_exact_collection()
33
33
{
34
-
$signal = new class extends Signal {
34
+
$signal = new class () extends Signal {
35
35
public function eventTypes(): array
36
36
{
37
37
return ['commit'];
···
66
66
/** @test */
67
67
public function it_can_filter_by_wildcard_collection()
68
68
{
69
-
$signalClass = new class extends Signal {
69
+
$signalClass = new class () extends Signal {
70
70
public function eventTypes(): array
71
71
{
72
72
return ['commit'];
···
84
84
};
85
85
86
86
// Create registry and register the signal
87
-
$registry = new \SocialDept\Signal\Services\SignalRegistry;
87
+
$registry = new \SocialDept\AtpSignals\Services\SignalRegistry();
88
88
$registry->register($signalClass::class);
89
89
90
90
// Test that it matches app.bsky.feed.post
+2
-2
tests/Unit/VarintTest.php
+2
-2
tests/Unit/VarintTest.php
···
2
2
3
3
declare(strict_types=1);
4
4
5
-
namespace SocialDept\Signal\Tests\Unit;
5
+
namespace SocialDept\AtpSignals\Tests\Unit;
6
6
7
7
use PHPUnit\Framework\TestCase;
8
8
use RuntimeException;
9
-
use SocialDept\Signal\Binary\Varint;
9
+
use SocialDept\AtpSignals\Binary\Varint;
10
10
11
11
class VarintTest extends TestCase
12
12
{