Status
Accepted for the current collector implementation. The decision covers collection and processing state. It does not claim that the upstream feed is always available or that a Durable Object makes external calls instantaneous.
Context
The collector needs to ask the upstream service for active vehicles and locations every 20 seconds. The work includes external HTTP requests, a D1 write, transit derivation, and an optional enqueue to the road processor. A scheduled invocation can be repeated, a manual request can arrive while a cycle is running, and any of those calls can fail after the source request has already returned data.
The system therefore needs two forms of protection. Scheduling state must survive the lifetime of a worker instance, and storage must decide whether a packet has already been seen. An in-memory “last report” check cannot provide either guarantee.
Decision
Use a singleton VehicleCollector Durable Object for collection. The worker obtains it with the vehicle-collector name. start() stores the enabled flag and sets an alarm at the next 20-second boundary. stop() stores the disabled flag before deleting the alarm. When an alarm fires, the collector checks that flag, arms the next alarm before external I/O, and then runs the collection cycle.
Use D1 as the final raw dedupe boundary. vehicle_observations has a unique constraint on (vehicle_id, reported_at), and the insert uses ON CONFLICT ... DO NOTHING. The collector reports inserted and duplicate counts rather than treating a repeated packet as an exceptional failure.
Persist raw observations before deriving transit intelligence. A failed raw batch prevents intelligence processing for that batch. A successful raw batch can still have off-route positions, GPS jumps, or per-observation derivation failures. Those outcomes remain visible in the stored position quality and collection summary.
Keep road processing in a separate RoadIntelligenceProcessor Durable Object. The collector enqueues it only after new raw observations are stored. The processor owns its backlog cursor, Valhalla call, match-run persistence, retry delay, and optional disabled state. A missing VALHALLA_URL disables that branch without stopping transit collection.
The request path uses service-binding RPCs to reach the collector. A public snapshot read can trigger a collection only when no snapshot exists; it does not create a second scheduling loop. Manual collectNow() calls share the current in-flight collection promise inside one live object instance.
Collection sequence
The sequence below answers one question: which actor owns each boundary in a collection cycle?
The order is the decision. The following excerpt is from VehicleCollector.alarm:
try { await this.ctx.storage.setAlarm(nextCollectionBoundary(Date.now()));} catch (error) { console.error("Failed to rearm collector alarm", formatError(error)); throw error;}If re-arming fails, the implementation throws because no replacement alarm exists. Once the next alarm is stored, a provider, D1, or downstream failure is logged without cancelling recurrence. This protects scheduling; it does not hide the failure from the status record.
Consequences
The same named Durable Object coordinates the enabled flag, alarm, last run, service status, and public snapshot. D1 supplies the durable duplicate decision, so repeated alarm delivery can safely result in duplicate counts. The collector can preserve a successful vehicle when another location request fails, and it can retain the last good snapshot when the schedule provider fails.
Raw storage becomes the replay boundary. Transit derivation can reject a position without discarding its coordinates and timestamps. The road processor can retry a transient Valhalla error without re-fetching upstream telemetry. Its successful match writes the match run, segment samples, and cursor in one D1 batch.
There are limits. The in-flight promise is process-local to the live Durable Object instance; it is not the database's dedupe mechanism. The unique key is based on the vehicle's reported time, so it cannot determine whether a provider has sent a late but different report. The alarm recurrence protects future work, but it cannot make an upstream response valid or make route geometry available.
Evidence and implementation history
The first collector change, 01b7781, established raw observations, D1 dedupe, and the recurring collector. 521b6db added transit derivation. The road processor and its retry/cursor behavior followed as a separate branch.
The current schema is in 0001_vehicle_observations.sql, 0003_transit_intelligence.sql, and 0004_road_intelligence.sql. Lifecycle behavior is covered by collector.test.ts.