crdt_socket_sync_dart_frog 0.1.0
crdt_socket_sync_dart_frog: ^0.1.0 copied to clipboard
Dart Frog adapter for crdt_socket_sync - serve CRDT sync and relay WebSocket sessions from a Dart Frog backend.
CRDT Socket Sync — Dart Frog #
What it does #
crdt_socket_sync ships two
WebSocket servers that own a dart:io HttpServer. Under
Dart Frog the HTTP server is the
framework's, so those two are not usable: there is no server to create and no
request to upgrade yourself.
This package bridges that. It takes the session hosts from crdt_socket_sync —
DocumentSessionHost (CRDT-aware) and RelaySessionHost (relay) — and gives
you a Dart Frog Handler that upgrades the request and hands the socket over.
Everything else is unchanged: same protocol, same handshake, same plugins, same
aligned snapshots and log compaction.
It is deliberately thin. The interesting code lives in crdt_socket_sync;
what this adds is the seam, plus the one thing Dart Frog gives you that the
bundled servers cannot — the request, before the socket exists.
Installation #
dart pub add crdt_socket_sync_dart_frog
One library per communication mode, like crdt_socket_sync itself:
// Server–client mode: crdtSyncWebSocketHandler, crdtSyncHostProvider,
// DocumentSessionHost
import 'package:crdt_socket_sync_dart_frog/sync.dart';
// Relay mode: crdtRelayWebSocketHandler, crdtRelayHostProvider,
// RelaySessionHost
import 'package:crdt_socket_sync_dart_frog/relay.dart';
Quick start #
Relay mode #
The relay never interprets CRDT data: it rebroadcasts opaque blobs to the other clients of a room and persists them. It is the simpler of the two.
// lib/src/host.dart — built once, shared by every request
import 'package:crdt_socket_sync/relay_server.dart';
final relayHost = RelaySessionHost(store: InMemoryRelayStore());
// routes/_middleware.dart
import 'package:crdt_socket_sync_dart_frog/relay.dart';
import 'package:dart_frog/dart_frog.dart';
import '../lib/src/host.dart';
Handler middleware(Handler handler) {
return handler.use(crdtRelayHostProvider(relayHost));
}
// routes/relay.dart
import 'package:crdt_socket_sync_dart_frog/relay.dart';
import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context) async {
return crdtRelayWebSocketHandler(context.read<RelaySessionHost>())(context);
}
Clients connect with WebSocketRelayClient from crdt_socket_sync, pointed at
ws://localhost:8080/relay.
Server–client mode #
The CRDT-aware mode keeps the documents server-side in a CRDTServerRegistry,
validates changes and takes aligned snapshots.
// lib/src/host.dart
import 'package:crdt_socket_sync/server.dart';
final syncHost = DocumentSessionHost(
serverRegistry: InMemoryCRDTServerRegistry(),
);
// routes/_middleware.dart
Handler middleware(Handler handler) {
return handler.use(crdtSyncHostProvider(syncHost));
}
// routes/sync.dart
import 'package:crdt_socket_sync_dart_frog/sync.dart';
import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context) async {
return crdtSyncWebSocketHandler(context.read<DocumentSessionHost>())(context);
}
Use a persistent registry (PersistentServerRegistry with crdt_lf_hive,
crdt_lf_sqlite, crdt_lf_drift) for anything that must survive a restart —
example/ does, with Hive. A registry is opened asynchronously,
so build it in Dart Frog's init() (see Lifecycle) and keep it
in a top-level late final.
Authenticating before the upgrade #
This is the reason to reach for Dart Frog here. The handler is just a value: call it only once the request has earned it.
Future<Response> onRequest(RequestContext context) async {
final token = context.request.headers['authorization'];
if (!await isAuthorized(token)) {
return Response(statusCode: HttpStatus.unauthorized);
}
return crdtSyncWebSocketHandler(context.read<DocumentSessionHost>())(context);
}
A rejected client gets a plain 401 and never reaches the protocol. With the
bundled WebSocketServer the socket is already up by the time you could look
at anything.
Routing and document ids #
The document id travels inside the protocol frames (the handshake in server–client mode, the hello in relay mode), not in the URL. A single route therefore serves every document, and this package does no routing of its own.
A parameterised route is still useful — for authorizing per document:
// routes/sync/[documentId].dart
Future<Response> onRequest(RequestContext context, String documentId) async {
if (!await canAccess(context, documentId)) {
return Response(statusCode: HttpStatus.forbidden);
}
return crdtSyncWebSocketHandler(context.read<DocumentSessionHost>())(context);
}
Note that the path segment is what you check; the host still takes the document id from the frames.
Lifecycle #
Build the host once, outside the request. A host per request would give every client its own empty room.
start() is optional: a host with no transport of its own starts on its first
connection. Call it explicitly from a
custom init method
when you also want an orderly shutdown — Dart Frog has no shutdown hook, so
signals are the place for it. init() runs once, before the handler tree is
built; the
custom entrypoint
run() is called again on every hot reload, so a listener registered there
would pile up:
// main.dart
import 'dart:io';
import 'package:dart_frog/dart_frog.dart';
Future<void> init(InternetAddress ip, int port) async {
await relayHost.start();
for (final signal in [ProcessSignal.sigint, ProcessSignal.sigterm]) {
signal.watch().listen((_) async {
await relayHost.dispose();
exit(0);
});
}
}
dispose() closes the open sessions and, in server–client mode, the registry
under them — for a durable registry that is where pending writes get flushed,
so skipping it loses data. A RelayStore has no close: a durable one is yours
to close after dispose().
Two kinds of ping #
crdtSyncWebSocketHandler and crdtRelayWebSocketHandler forward protocols,
allowedOrigins and pingInterval to dart_frog_web_socket. Set
allowedOrigins if browsers connect; unset, any origin may.
pingInterval is a WebSocket-level ping. It is not the protocol's own ping
(Protocol.pingInterval, 15s), which carries the client's version vector and
drives both liveness detection and the aligned-snapshot coordinator. They are
complementary — setting one is not a reason to drop the other.
Gotchas #
Handleris ambiguous.crdt_lfexports a CRDTHandleranddart_frogexports a requestHandler. In a file that needs both, hide one:import 'package:crdt_lf/crdt_lf.dart' hide Handler;.- One plugin instance per host. A
ServerSyncPluginbinds to the host it is given to, once; handing the same instance to a second host throws aLateInitializationErrorfar from the line that caused it. - A refused connection is closed, not rejected. Once the response is a
101, there is no status code left to send. A host that is stopped or disposed closes the socket instead, which the client sees as its stream ending.
Examples #
Two runnable Dart Frog projects, one per mode, each with a custom init()
that starts the host and a custom entrypoint:
example/— server–client mode on/sync, documents kept in Hive throughcrdt_lf_hive, a token check in front of the route.relay_example/— relay mode on/relay, rooms kept in memory.
cd example # or relay_example
dart_frog dev
Roadmap #
A roadmap is available in the project page.
Apps #
- greyhound_markdown — Real-time collaborative markdown editor built on crdt_lf
Packages #
Other bricks of the crdt "system" are: