Skip to content

DevTools API Reference ​

@korajs/devtools records Kora's instrumentation events and shows them in a browser DevTools panel or an in-page overlay. createApp({ devtools: true }) sets it up; see the DevTools guide for using the panels.

Events ​

Kora reports what happens to your data as typed events (KoraEvent in @korajs/core). Subscribe on the app:

typescript
const off = app.events.on('sync:operation-rejected', (event) => {
  console.warn(`${event.collection}/${event.recordId} was refused: ${event.code}`)
})
app.on('merge:conflict', (event) => console.log(event.trace.strategy))
off()

app.on is the same as app.events.on. A sync server emits its events on the emitter passed to KoraSyncServer / createProductionServer. The DevTools column says whether the Instrumenter records the event for the panel.

Operations and merges ​

EventPayloadEmittedDevTools
operation:createdoperationafter every local writeyes
operation:appliedoperation, durationafter the store applies a remote operation (not for local writes)yes
merge:startedoperationA, operationBbefore the conflict traces of an applied remote operationyes
merge:conflicttrace (MergeTrace)once per fold decision that was a conflictyes
merge:completedtraceafter the traces of one mergeyes
constraint:violatedconstraint, tracea device's optimistic constraint check failed after applying a remote operation (the server is the authority)yes
replay:completedtargetOperationId, operationsApplied, durationapp.replayTo() finishedyes

Sync connection ​

EventPayloadEmittedDevTools
sync:connectednodeIdhandshake acceptedyes
sync:disconnectedreasonconnection closedyes
sync:suspendedreason (auth-loading, auth-required, auth-rejected, device-revoked, encryption-locked, ...)sync is paused on purposeyes
sync:auth-failedreasonthe server refused the credentialsyes
sync:schema-mismatchclientSchemaVersion, serverSchemaVersion, supportedMin, supportedMax, reasonthe server does not accept this schema versionyes
sync:clock-skewskewMs, severity (info, slow-warning, fast-blocked), sourceclock skew measured at the handshake or reported by the serverno
sync:clock-rebaserebasedCount, maxSkewMsunsynced future-dated operations were re-stampedno
sync:node-id-rotatedpreviousNodeId, nodeId, reenqueuedCount, heldCount?the server refused this device's node id; writes moved to a new nodeno
sync:local-nodenodeId, action, localSequence?, serverSequence?, operationCount?bookkeeping of this database's nodes: history-behind, server-behind, adoption-*, held, clone-detected, principal-switched, held-assigned, held-discarded, retired-repaired (storedCount?, reauthoredUnder?: a retired node's writes the server holds, and where the rest were re-authored), queue-settled (storedCount?: operations of a node known only from queue entries that the server holds, settled; the rest stay held)no
connection:qualityqualitythe measured quality changedyes
sync:diagnosticsdiagnosticsperiodic metrics snapshotyes
sync:bandwidthbytesPerSecond, directionbandwidth sampleyes
encryption:statusstatus (state, keyring, keyVersion, code?, message?)the encryption keyring locked, unlocked or failedno

Sync data flow ​

EventPayloadEmittedDevTools
sync:sentoperations, batchSizea batch was uploadedyes
sync:receivedoperations, batchSize (server: also uniqueOperations, duplicateOperations, rejectedOperations)a batch arrivedyes
sync:acknowledgedsequenceNumberthe server acknowledged an uploadyes
sync:operations-acknowledgedoperationIdsthe server stored these operations of this device (they can no longer be refused)yes
sync:operations-rewrittenreason (clock-rebase, node-rotation, resequence), idMappingunsent operations were re-authored under new ids before upload; later events name the new idsno
sync:initial-sync-progressprogress, totalBatches, receivedBatchesduring the first syncyes
sync:operation-rejectedoperationId, collection, recordId, code, message, retriablethe server refused one of this device's operations; it is kept in app.sync.getRejectedOperations(), not retriedno
sync:apply-failedoperationId, collection, recordId, code, message, retriablea received operation could not be applied (it is retried or quarantined, never skipped)yes
sync:apply-blocked / sync:apply-retrying / sync:apply-recoveredfailurea failed apply blocks delivery, is retried, or succeededyes
sync:delivery-gapexpectedBase, receivedBase, currentWatermark, messageId, repeatCounta batch did not continue the delivery watermark; it is re-requestedno
sync:scope-retractedcollection, recordId, quarantinedOperationIdsa record left this device's sync scopeno
sync:durability-degraded / sync:durability-restoredmessage, failedAttempts / nonethe local database could not be made durable before uploads (the server holds the durable copy), and recoveryyes

Server only ​

EventPayloadEmitted
sync:protocol-deprecatednodeId, clientProtocolVersion, serverProtocolVersion, messagea protocol-1 client (Kora 1.0.0-beta.12 or earlier) connected
sync:unverified-legacy-operationnodeId, operationId, collection, messagea protocol-1 operation whose id could not be verified was stored
sync:forged-duplicatenodeId, operationId, collection, messagean upload reused a stored operation id with different content (refused)
server:commit-hook-failedsource, sessionId, operationIds, messagean onOperationsCommitted handler threw or rejected (the write was committed regardless)
server:tombstone-delivery-undeterminedsessionId, collection, recordId, operationId, reason, action, messagethe server could not determine where a record's deletion began for an access-rules member; the member's stream was held before the write (held), or, when the hold could not resolve or the stream cannot be held, the write was sent (sent, logged as an error)
sync:delivery-stalledsessionId, watermark, outstandingMaxDeliverySequence, repeatCount, reasona client stopped acknowledging deliveries

Blobs ​

EventPayloadEmittedDevTools
blob:request-retriedhash, requestId, attempt, reason (timeout, not-connected, reconnected, throttled)a blob chunk request was re-sent under the same request id; attempt counts requests actually sent, this one included (tries made offline are not counted and emit nothing)yes
blob:handler-errorphase (ready, message), messageType, code, messagea handler on the blob chunk channel threw; the other handlers still ranyes
blob:request-failedhash, requestId, attempts, code (BLOB_CHUNK_TIMEOUT, BLOB_CHUNK_UNAVAILABLE, BLOB_CHUNK_CANCELLED), reason, messagea chunk request ended without bytes; app.blobs.pull rejects with BlobTransferErroryes

Storage ​

EventPayloadEmittedDevTools
store:durability-lostdbName, phase (open, promotion), reason, messageblocking: no durable storage; writes are refused with StorageDurabilityError unless allowNonDurableyes
store:storage-blockeddbName, resource, state (waiting, resolved), waitedMs?, messageblocking while waiting: another holder has the database's OPFS storageyes
store:opfs-unavailabledbName, reason, messageOPFS could not be used and the store is not durable (emitted with store:durability-lost)yes
store:storage-fallbackdbName, from, to, reason, messageOPFS was unavailable and the app opened on durable IndexedDB insteadyes
store:storage-migrateddbName, from, to, messagedata moved between storage locations (one copy remains)yes
store:schema-aheaddbName, storedVersion, codeVersion, messagethe database was migrated by a newer build; the store refused to open it (SchemaVersionAheadError)yes
store:format-aheaddbName, storedFormat, codeFormat, messageblocking: a build with a newer storage format opened the database; refused (StorageFormatAheadError)yes
store:build-gate-waitingdbName, waitingFor (tab identities), reason (live-writer, unsaved-writes, legacy-build), messageblocking while waiting: this tab waits for tabs of another build to stop writing (after 10 s, again when the reason changes)yes
store:build-unidentifieddbName, fingerprint, messageno build id: tabs are told apart by a schema fingerprint onlyyes
store:legacy-build-detecteddbName, messagea tab of a pre-gate build leads the database; this tab waits until it closesyes
store:tab-exclusivedbName, heldBy, messageblocking while waiting: no BroadcastChannel, one tab at a timeyes
store:tab-coordination-unavailabledbName, messageblocking: no Web Locks; refused (TabCoordinationUnavailableError) unless allowUncoordinatedTabsyes
app:build-supersededcurrentBuild, newerBuild, preventDefault(), defaultPreventedthis tab was superseded by a newer build; emitted before any write is refusedyes
app:before-build-reloadnoneapp.reloadForNewBuild() is about to reload (save form drafts here)yes
store:db-name-collisiondbName, messageanother runtime on this origin uses this database name (shared on purpose for tabs of one app; a bug for separate apps)yes
store:persistence-errordbName, message, codepersisting failedyes
store:quota-exceededdbName, messagea write exceeded the storage quotayes
store:log-integritydbName, repaired, quarantined, gaps, clean, messagethe open-time log scan repaired or quarantined rowsyes
store:rematerializeddbName, mode, records, changedRows, messagerecords were rebuilt with the fold (first open after upgrading, or after a restore)yes
storage:persistencestate, persisted, message?the navigator.storage persistence check or requestyes

Queries and presence ​

EventPayloadEmittedDevTools
query:errorqueryId, collection, phase, code, messagea subscription failed (it keeps its last results)yes
awareness:updatedstatespresence states changedno

Declared but not emitted ​

query:subscribed, query:invalidated, query:executed, state-machine:transition, state-machine:rejected and sync:apply-abandoned are part of the KoraEvent type but no Kora package emits them in 1.0.0-beta.13. Do not build on them. State-machine refusals surface as InvalidStateTransitionError from the write.


createApp integration ​

With devtools: true, createApp creates an Instrumenter on the app's emitter, forwards events to the browser extension through window.postMessage and mounts the in-page overlay (toggle with Ctrl+Shift+K, Cmd+Shift+K on macOS). In Node and server renders only the instrumenter runs.

Instrumenter ​

typescript
class Instrumenter {
  constructor(emitter: KoraEventEmitter, config?: {
    bufferSize?: number      // default 10000
    bridgeEnabled?: boolean  // default true: post events through a MessageBridge
    channelName?: string     // default 'kora-devtools'
  })
}

Methods: getBuffer(), getBridge(), pause(), resume(), isPaused(), destroy(). Events received while paused are dropped. The recorded types are listed in the tables above.

typescript
import { Instrumenter } from '@korajs/devtools'

const instrumenter = new Instrumenter(app.events, { bufferSize: 5000, bridgeEnabled: false })
const recent = instrumenter.getBuffer().getAll()
instrumenter.destroy()

Building blocks ​

ExportDescription
EventBuffer(capacity = 10000)Ring buffer of TimestampedEvent ({ id, event, receivedAt }): push, getAll, getRange(startId, endId), getByType, clear, size, capacity.
MessageBridge(channelName = 'kora-devtools')window.postMessage transport: send, onReceive, destroy; a no-op without window.
filterEvents(events, { categories?, types?, timeRange?, collection? })AND-combined filter.
getEventCategory(type)'operation' | 'merge' | 'sync' | 'query' | 'connection'.
computeStatistics(events)Counts by category and type, merge conflicts, constraint violations, average merge and query durations, sync totals.
buildPanelModel(events){ timeline, conflicts, operations, network }, the data behind the panels.
renderDevtoolsPanel(target, events)Renders the panel with Preact.
mountKoraDevtoolsOverlay(instrumenter) (@korajs/devtools/overlay)Mounts the overlay; returns a teardown function.
PortRouterRoutes kora-content ports to the kora-panel port of the same tab in the extension's background script.