summaryrefslogtreecommitdiff
path: root/src/channel
Commit message (Collapse)AuthorAge
...
* Stray warningsOwen Jacobson2024-10-03
|
* List messages per channel.Owen Jacobson2024-10-03
|
* Add endpoints for deleting channels and messages.Owen Jacobson2024-10-03
| | | | It is deliberate that the expire() functions do not use them. To avoid races, the transactions must be committed before events get sent, in both cases, which makes them structurally pretty different.
* Represent channels and messages using a split "History" and "Snapshot" model.Owen Jacobson2024-10-03
| | | | | | This separates the code that figures out what happened to an entity from the code that represents it to a user, and makes it easier to compute a snapshot at a point in time (for things like bootstrap). It also makes the internal logic a bit easier to follow, since it's easier to tell whether you're working with a point in time or with the whole recorded history. This hefty.
* Package up common event fields as InstantOwen Jacobson2024-10-02
|
* Retire top-level `repo`.Owen Jacobson2024-10-02
| | | | This helped me discover an organizational scheme I like more.
* First pass on reorganizing the backend.Owen Jacobson2024-10-02
| | | | This is primarily renames and repackagings.
* Organize IDs into top-level namespaces.Owen Jacobson2024-10-01
| | | | (This is part of a larger reorganization.)
* Provide a resume point to bridge clients from state snapshots to the event ↵Owen Jacobson2024-10-01
| | | | sequence.
* Track event sequences globally, not per channel.Owen Jacobson2024-10-01
| | | | Per-channel event sequences were a cute idea, but it made reasoning about event resumption much, much harder (case in point: recovering the order of events in a partially-ordered collection is quadratic, since it's basically graph sort). The minor overhead of a global sequence number is likely tolerable, and this simplifies both the API and the internals.
* Shut down the `/api/events` stream when the user logs out or their token ↵Owen Jacobson2024-09-29
| | | | | | | | expires. When tokens are revoked (logout or expiry), the server now publishes an internal event via the new `logins` event broadcaster. These events are used to guard the `/api/events` stream. When a token revocation event arrives for the token used to subscribe to the stream, the stream is cut short, disconnecting the client. In service of this, tokens now have IDs, which are non-confidential values that can be used to discuss tokens without their secrets being passed around unnecessarily. These IDs are not (at this time) exposed to clients, but they could be.
* Expire channels, too.Owen Jacobson2024-09-28
|
* Delete expired messages out of band.Owen Jacobson2024-09-28
| | | | | | | | Trying to reliably do expiry mid-request was causing some anomalies: * Creating a channel with a dup name would fail, then succeed after listing channels. It was very hard to reason about which operations needed to trigger expiry, to fix this "correctly," so now expiry runs on every request.
* Push message body into its own object in eventsOwen Jacobson2024-09-28
|
* Send created events when channels are added.Owen Jacobson2024-09-28
|
* Make `/api/events` a firehose endpoint.Owen Jacobson2024-09-27
| | | | | | | | It now includes events for all channels. Clients are responsible for filtering. The schema for channel events has changed; it now includes a channel name and ID, in the same format as the sender's name and ID. They also now include a `"type"` field, whose only valid value (as of this writing) is `"message"`. This is groundwork for delivering message deletion (expiry) events to clients, and notifying clients of channel lifecycle events.
* Retire `fixtures::error::expected!`.Owen Jacobson2024-09-25
| | | | I had no idea `std` included a `matches!` macro, and I feel we're better off using it.
* More reorganizing.Owen Jacobson2024-09-25
|
* Crank up the Clippy warnings.Owen Jacobson2024-09-25
| | | | This'll catch style issues, mostly.
* Code organization changes considered during implementation of ↵Owen Jacobson2024-09-25
| | | | vector-of-sequence-numbers stream resume.
* Redundant code missed in previous commit.Owen Jacobson2024-09-25
|
* Use a vector of sequence numbers, not timestamps, to restart /api/events ↵Owen Jacobson2024-09-25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | streams. The timestamp-based approach had some formal problems. In particular, it assumed that time always went forwards, which isn't necessarily the case: * Alice calls `/api/channels/Cfoo` to send a message. * The server assigns time T to the request. * The server stalls somewhere in send() for a while, before storing and broadcasting the message. If it helps, imagine blocking on `tx.begin().await?` for a while. * In this interval, Bob calls `/api/events?channel=Cfoo`, receives historical messages up to time U (after T), and disconnects. * The server resumes Alice's request and finishes it. * Bob reconnects, setting his Last-Event-Id header to timestamp U. In this scenario, Bob never sees Alice's message unless he starts over. It wasn't in the original stream, since it wasn't broadcast while Bob was subscribed, and it's not in the new stream, since Bob's resume point is after the timestamp on Alice's message. The new approach avoids this. Each message is assigned a _sequence number_ when it's stored. Bob can be sure that his stream included every event, since the resume point is identified by sequence number even if the server processes them out of chronological order: * Alice calls `/api/channels/Cfoo` to send a message. * The server assigns time T to the request. * The server stalls somewhere in send() for a while, before storing and broadcasting. * In this interval, Bob calls `/api/events?channel=Cfoo`, receives historical messages up to sequence Cfoo=N, and disconnects. * The server resumes Alice's request, assigns her message sequence M (after N), and finishes it. * Bob resumes his subscription at Cfoo=N. * Bob receives Alice's message at Cfoo=M. There's a natural mutual exclusion on sequence numbers, enforced by sqlite, which ensures that no two messages have the same sequence number. Since sqlite promises that transactions are serializable by default (and enforces this with a whole-DB write lock), we can be confident that sequence numbers are monotonic, as well. This scenario is, to put it mildly, contrived and unlikely - which is what motivated me to fix it. These kinds of bugs are fiendishly hard to identify, let alone reproduce or understand. I wonder how costly cloning a map is going to turn out to be… A note on database migrations: sqlite3 really, truly has no `alter table … alter column` statement. The only way to modify an existing column is to add the column to a new table. If `alter column` existed, I would create the new `sequence` column in `message` in a much less roundabout way. Fortunately, these migrations assume that they are being run _offline_, so operations like "replace the whole table" are reasonable.
* Write tests.Owen Jacobson2024-09-20
|
* Push the handling of the `Last-Event-Id` _format_ inside of `channels::app`.Owen Jacobson2024-09-20
| | | | This is intended to make it a bit more opaque to callers, and to free me up to experiment with the event ID format. It also makes event IDs tractable for testing.
* Push events into a module structure consistent with the rest of the project.Owen Jacobson2024-09-20
|
* Remove the HTML client, and expose a JSON API.Owen Jacobson2024-09-20
| | | | | | | | | | | | | This API structure fell out of a conversation with Kit. Described loosely: kit: ok kit: Here's what I'm picturing in a client kit: list channels, make-new-channel, zero to one active channels, post-to-active. kit: login/sign-up, logout owen: you will likely also want "am I logged in" here kit: sure, whoami
* Expire messages after 90 days.Owen Jacobson2024-09-20
| | | | | | | | | | This is intended to manage storage growth. A community with broadly steady traffic will now reach a steady state (ish) where the amount of storage in use stays within a steady band. The 90 day threshold is a spitball; this should be made configurable for the community's needs. I've also hoisted expiry out into the `app` classes, to reduce the amount of non-database work repo types are doing. This should make it easier to make expiry configurable later on. Includes incidental cleanup and style changes.
* Somewhere along the line this lifetime bound became redundant.Owen Jacobson2024-09-18
|
* Most pass-through errors do not need additional message textOwen Jacobson2024-09-18
|
* Return 404s when resources are not found.Owen Jacobson2024-09-18
| | | | This is implemented by making the return values, in most cases, idiosyncratic ad-hoc types that then convert to the approprate error response. This also should make endpoints more testable, since the return values can now be inspected to check their properties without having to process or parse an HTTP response.
* Make BoxedError an implementation detail of InternalError.Owen Jacobson2024-09-18
|
* App methods now return errors that allow not-found cases to be distinguished.Owen Jacobson2024-09-18
|
* Express record dependencies through types.Owen Jacobson2024-09-17
| | | | This provides a convenient place to _stick_ "not found" errors, though actually introducing them will come in a later commit.
* Consolidate most repository types into a repo module.Owen Jacobson2024-09-16
| | | | | | | | | | | | Having them contained in the individual endpoint groups conveyed an unintended sense that their intended scope was _only_ that endpoint group. It also made most repo-related import paths _quite_ long. This splits up the repos as follows: * "General applicability" repos - those that are only loosely connected to a single task, and are likely to be shared between tasks - go in crate::repo. * Specialized repos - those tightly connected to a specific task - go in the module for that task, under crate::PATH::repo. In both cases, each repo goes in its own submodule, to make it easier to use the module name as a namespace. Which category a repo goes in is a judgment call. `crate::channel::repo::broadcast` (formerly `channel::repo::messages`) is used outside of `crate::channel`, for example, but its main purpose is to support channel message broadcasts. It could arguably live under `crate::event::repo::channel`, but the resulting namespace is less legible to me.
* Expose sqlx errors directly in repo interfaces.Owen Jacobson2024-09-16
| | | | BoxedError conceals the exact nature of the error, which in turn prevents me from using sqlx::Error::RowNotFound to signal absences.
* Annotate channel events with channel ID at the router, not intrinsically.Owen Jacobson2024-09-15
| | | | This bugged me aesthetically. At `app.channel().events(channel)`, the caller knows the channel ID; they don't need to be told. Having the same info come back out in the returned events felt bad.
* Move channel list into the `app.channels()` namespace.Owen Jacobson2024-09-15
| | | | This is groundwork for a JSON-based API, after a conversation with Kit.
* Consolidate channel events into a single stream endpoint.Owen Jacobson2024-09-15
| | | | | | | | | | | | | | While reviewing [MDN], I noticed this note: > SSE suffers from a limitation to the maximum number of open connections, which can be specially painful when opening various tabs as the limit is per browser and set to a very low number (6). […] This limit is per browser + domain, so that means that you can open 6 SSE connections across all of the tabs to www.example1.com and another 6 SSE connections to www.example2.com. I tested it in Safari; this is true, and once six streams are open, _no_ more requests can be made - in any tab, even a fresh one. Since the design _was_ that each channel had its own events endpoint, this is an obvious operations risk. Any client that tries to read multiple channels' streams will hit this limit quickly. This change consolidates all channel events into a single endpoint: `/events`. This takes a list of channel IDs (as query parameters, one `channel=` param per channel), and streams back events from all listed channels. The previous `/:channel/events` endpoint has been removed. Clients can selectively request events for the channels they're interested in. [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/EventSource
* Placeholder UX, probablyOwen Jacobson2024-09-14
|
* Support Last-Event-Id as a method of resuming channel events after a disconnectOwen Jacobson2024-09-13
|
* Generate the required structure for broadcasting from a join, not from O(n) ↵Owen Jacobson2024-09-13
| | | | queries.
* Tolerate panics in channel::app where they can only be triggered by ↵Owen Jacobson2024-09-13
| | | | implementation errors.
* Suggested fixes from Clippy, via nursery and pedantic sets.Owen Jacobson2024-09-13
|
* Embed the sender's whole login (id and name) in messages, drop the redundant ↵Owen Jacobson2024-09-13
| | | | channel ID.
* Transmit messages via `/:chan/send` and `/:chan/events`.Owen Jacobson2024-09-13
|
* Push most endpoint and extractor logic into functoins of `App`.Owen Jacobson2024-09-12
| | | | | | | | This is, again, groundwork for logic that requires more than just a database connection. The login process has been changed to be more conventional, attempting login _before_ account creation rather than after it. This was not previously possible, because the data access methods used to perform these steps did not return enough information to carry out the workflow in that order. Separating storage from password validation and hashing forces the issue, and makes it clearer _at the App_ whether an account exists or not. This does introduce the possibility of two racing inserts trying to lay claim to the same username. Transaction isolation should ensure that only one of them "wins," which is what you get before this change anyways.
* Wrap the database pool in an App struct.Owen Jacobson2024-09-12
| | | | | | This is a jumping-off point for adding logic that needs more than just the DB for state, such as chat message handling. The name sucks, but it's the best I've got.
* Remove the notion of "channel members."Owen Jacobson2024-09-11
| | | | | | This came out of a conversation with Kit. Their position, loosely, was that seeing scrollback when you look at a channel is useful, and since message delivery isn't meaningfully tied to membership (or at least doesn't have to be), what the hell is membership even doing? (I may have added that last part.) My take, on top of that, is that membership increases the amount of concepts we're committed to. We don't need that commitment yet.
* Support joining channels.Owen Jacobson2024-09-04
|
* Support leaving a channelOwen Jacobson2024-09-04
|