Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel
1<?php
2
3declare(strict_types=1);
4
5namespace SocialDept\AtpSignals\Binary;
6
7use RuntimeException;
8
9/**
10 * Binary data reader with position tracking.
11 *
12 * Provides stream-like interface for reading from binary strings.
13 */
14class Reader
15{
16 private int $position = 0;
17
18 public function __construct(
19 private readonly string $data,
20 ) {
21 }
22
23 /**
24 * Get current position in the data.
25 */
26 public function getPosition(): int
27 {
28 return $this->position;
29 }
30
31 /**
32 * Get total length of data.
33 */
34 public function getLength(): int
35 {
36 return strlen($this->data);
37 }
38
39 /**
40 * Check if there's more data to read.
41 */
42 public function hasMore(): bool
43 {
44 return $this->position < strlen($this->data);
45 }
46
47 /**
48 * Get remaining bytes count.
49 */
50 public function remaining(): int
51 {
52 return strlen($this->data) - $this->position;
53 }
54
55 /**
56 * Peek at the next byte without advancing position.
57 *
58 * @throws RuntimeException If no more data available
59 */
60 public function peek(): int
61 {
62 if (! $this->hasMore()) {
63 throw new RuntimeException('Unexpected end of data');
64 }
65
66 return ord($this->data[$this->position]);
67 }
68
69 /**
70 * Read a single byte and advance position.
71 *
72 * @throws RuntimeException If no more data available
73 */
74 public function readByte(): int
75 {
76 $byte = $this->peek();
77 $this->position++;
78
79 return $byte;
80 }
81
82 /**
83 * Read exactly N bytes and advance position.
84 *
85 * @throws RuntimeException If not enough data available
86 */
87 public function readBytes(int $length): string
88 {
89 if ($this->remaining() < $length) {
90 throw new RuntimeException("Cannot read {$length} bytes, only {$this->remaining()} remaining");
91 }
92
93 $bytes = substr($this->data, $this->position, $length);
94 $this->position += $length;
95
96 return $bytes;
97 }
98
99 /**
100 * Read a varint (variable-length integer).
101 *
102 * @throws RuntimeException If varint is malformed
103 */
104 public function readVarint(): int
105 {
106 return Varint::decode($this->data, $this->position);
107 }
108
109 /**
110 * Get all remaining data without advancing position.
111 */
112 public function peekRemaining(): string
113 {
114 return substr($this->data, $this->position);
115 }
116
117 /**
118 * Read all remaining data and advance position to end.
119 */
120 public function readRemaining(): string
121 {
122 $remaining = $this->peekRemaining();
123 $this->position = strlen($this->data);
124
125 return $remaining;
126 }
127
128 /**
129 * Skip N bytes forward.
130 *
131 * @throws RuntimeException If trying to skip past end
132 */
133 public function skip(int $bytes): void
134 {
135 if ($this->remaining() < $bytes) {
136 throw new RuntimeException("Cannot skip {$bytes} bytes, only {$this->remaining()} remaining");
137 }
138
139 $this->position += $bytes;
140 }
141}