Liking cljdoc? Tell your friends :D

dk.simongray.like-minded

Syncing a state of mind-meld between a user's devices, through any number of backends.

A backend is a map of two functions and a name:

{:pull (fn [state cursor at])  ; the states of the other devices
 :push (fn [state cursor at])  ; writes the merged state
 :name "Nextcloud"}

A pull returns a map of the :states to merge, the next :cursor and a :report. It has :partial? when the states are changes since the cursor rather than whole states. A push returns the next :cursor and a :report, and a :state when it learned more while writing. Both take the instant of the sync and never read a clock. The function backend makes a backend of any store of dk.simongray.like-minded.store.

The options of the state say what the state is. Give the same ones to a backend, to sync!, to reconcile and to the functions of dk.simongray.like-minded.document:

  • :schema, the schema of a state of mind-meld, whose functions merge it
  • :crdt, the :merge, :compact and :problem functions of another CRDT, which go over those of a :schema
  • :version, the version of the state's format, document/version by default
  • :kind, what state the documents hold, e.g. "my-app/notes", so that a document of another kind is left out
  • :codec, how a document is encoded, document/edn-codec by default

A sync needs a :schema, or all three functions of a :crdt.

Syncing a state of mind-meld between a user's devices, through any
number of backends.

A backend is a map of two functions and a name:

    {:pull (fn [state cursor at])  ; the states of the other devices
     :push (fn [state cursor at])  ; writes the merged state
     :name "Nextcloud"}

A pull returns a map of the :states to merge, the next :cursor and a
:report. It has :partial? when the states are changes since the cursor
rather than whole states. A push returns the next :cursor and a
:report, and a :state when it learned more while writing. Both take the
instant of the sync and never read a clock. The function backend makes
a backend of any store of dk.simongray.like-minded.store.

The options of the state say what the state is. Give the same ones to a
backend, to sync!, to reconcile and to the functions of
dk.simongray.like-minded.document:

- :schema, the schema of a state of mind-meld, whose functions merge it
- :crdt, the :merge, :compact and :problem functions of another CRDT,
  which go over those of a :schema
- :version, the version of the state's format, document/version by
  default
- :kind, what state the documents hold, e.g. "my-app/notes", so that a
  document of another kind is left out
- :codec, how a document is encoded, document/edn-codec by default

A sync needs a :schema, or all three functions of a :crdt.
raw docstring

backendclj/s

(backend store {:keys [strategy direction] :as opts})

A backend over store, a document store of dk.simongray.like-minded.store, with the options of the state and the other opts below:

  • :strategy, :per-device for a document per device, or :shared for one document for all
  • :direction, :pull to read only, :push to write only, or :both
  • :app, the name of the app in the documents written
  • :name, the store's name by default, and distinct for each backend of one sync
  • :document, the name of the shared document, state and the extension of the codec by default
  • :extra-documents-fn, a function of the state that gives documents to write beside those of the devices, as a map of name to body, e.g. for other apps to read

The :strategy is :per-device when the store has :list, and else :shared. With a document per device, every device writes its own, named after its device id, and reads those of the others. So no two devices write the same document, and a folder that any sync client mirrors is a complete backend. One shared document is put on condition of the version that was fetched, and merged again on a conflict. The :direction is :both when the store has :put, and else :pull.

An extra document is written when it changed, and can't have the extension of the codec, since it would be read as a device's.

A backend over `store`, a document store of dk.simongray.like-minded.store,
with the options of the state and the other `opts` below:

- :strategy, :per-device for a document per device, or :shared for one
  document for all
- :direction, :pull to read only, :push to write only, or :both
- :app, the name of the app in the documents written
- :name, the store's name by default, and distinct for each backend of
  one sync
- :document, the name of the shared document, state and the extension
  of the codec by default
- :extra-documents-fn, a function of the state that gives documents to
  write beside those of the devices, as a map of name to body, e.g. for
  other apps to read

The :strategy is :per-device when the store has :list, and else
:shared. With a document per device, every device writes its own,
named after its device id, and reads those of the others. So no two
devices write the same document, and a folder that any sync client
mirrors is a complete backend. One shared document is put on condition
of the version that was fetched, and merged again on a conflict. The
:direction is :both when the store has :put, and else :pull.

An extra document is written when it changed, and can't have the
extension of the codec, since it would be read as a device's.
sourceraw docstring

default-cadenceclj/s

How many seconds to wait before syncing again:

  • :after-change, after a change to the local state
  • :idle, when nothing changed
  • :after-failure, after each failure in a row, repeating the last wait
How many seconds to wait before syncing again:

- :after-change, after a change to the local state
- :idle, when nothing changed
- :after-failure, after each failure in a row, repeating the last wait
sourceraw docstring

finishclj/s

(finish state step {:keys [compact] :as opts})

The state as it is by now, with the :state of the step of sync! merged in by the CRDT of opts, the options of the step, and compacted at the step's :at as the step was.

What the user did during the step is in state alone, so the result keeps both. The merge brings back the tombstones that the step dropped, which is why it's compacted again.

The `state` as it is by now, with the :state of the `step` of sync!
merged in by the CRDT of `opts`, the options of the step, and compacted
at the step's :at as the step was.

What the user did during the step is in `state` alone, so the result
keeps both. The merge brings back the tombstones that the step dropped,
which is why it's compacted again.
sourceraw docstring

next-syncclj/s

(next-sync step at)
(next-sync {:keys [changed? failures] synced-at :at :as step} at cadence)

When to sync again at at, after the last sync step, with the waits of cadence.

The wait depends on how many :failures in a row the step ended with, and on whether the local state has :changed? since, which the step of sync! doesn't know, so add it. The result is never before at, and without a step, it's at. The cadence goes over the defaults. The library schedules nothing, so run the sync from a timer of your own.

When to sync again at `at`, after the last sync `step`, with the waits
of `cadence`.

The wait depends on how many :failures in a row the step ended with,
and on whether the local state has :changed? since, which the step of
sync! doesn't know, so add it. The result is never before `at`, and
without a step, it's `at`. The `cadence` goes over the defaults. The
library schedules nothing, so run the sync from a timer of your own.
sourceraw docstring

reconcileclj/s

(reconcile state fetched at opts)

The state with the documents fetched merged in at at with opts, as a map of the :state and a :report, for an app that fetches and puts the documents itself.

The documents are maps of :name and :body. What they tell is acknowledged in the device registry, and the tombstones that every device has seen are compacted away. The opts are the options of the state, and :compact, false to keep the tombstones, or else the options of the compact function of the CRDT. The document of this device to put afterwards is what document/for-device gives.

The `state` with the documents `fetched` merged in at `at` with `opts`,
as a map of the :state and a :report, for an app that fetches and puts
the documents itself.

The documents are maps of :name and :body. What they tell is
acknowledged in the device registry, and the tombstones that every
device has seen are compacted away. The `opts` are the options of the
state, and :compact, false to keep the tombstones, or else the options
of the compact function of the CRDT. The document of this device to put
afterwards is what document/for-device gives.
sourceraw docstring

sync!clj/s

(sync! state backends {:keys [at cursors failures] :as opts})

Sync state through backends in one step with the opts below: pull from every backend at once, merge, and push the merged state to each.

It returns this, in ClojureScript as a promise:

{:state    merged
 :cursors  {"Nextcloud" {…} …}  ; by backend name
 :at       #inst "…"           ; the instant of the step
 :failures 0                    ; the steps in a row in which a
                                ; backend failed
 :report   [...]}               ; what reading and writing found

In ClojureScript, each function of a backend can return a value or a promise. The opts are the options of the state, whose CRDT merges, and these:

  • :at, the instant of the step, the time of the clock by default
  • :cursors and :failures, those of the last step
  • :compact, false to keep the tombstones that every device has seen, or else the options of the compact function of the CRDT

A backend whose pull fails isn't pushed to and keeps its cursor, and one whose push fails keeps the cursor of its pull. The report has a :sync/backend-failed diagnostic for each, with its :backend and the :reason. The state is only ever merged into, so a failed step loses nothing. A state in which the :problem of the CRDT finds a problem isn't pushed, since the other devices would leave it out, and the report has :sync/malformed-state.

Run one step at a time, since a step that overlaps another can write an older state after a newer one. What the user did during a step isn't in its :state, so don't put the result in place of the state, but give it to finish with the state as it is by then.

The backends need distinct names, since the cursors are kept by name, and the same name twice throws.

Sync `state` through `backends` in one step with the `opts` below: pull
from every backend at once, merge, and push the merged state to each.

It returns this, in ClojureScript as a promise:

    {:state    merged
     :cursors  {"Nextcloud" {…} …}  ; by backend name
     :at       #inst "…"           ; the instant of the step
     :failures 0                    ; the steps in a row in which a
                                    ; backend failed
     :report   [...]}               ; what reading and writing found

In ClojureScript, each function of a backend can return a value or a
promise. The `opts` are the options of the state, whose CRDT merges,
and these:

- :at, the instant of the step, the time of the clock by default
- :cursors and :failures, those of the last step
- :compact, false to keep the tombstones that every device has seen, or
  else the options of the compact function of the CRDT

A backend whose pull fails isn't pushed to and keeps its cursor, and one
whose push fails keeps the cursor of its pull. The report has a
:sync/backend-failed diagnostic for each, with its :backend and the
:reason. The state is only ever merged into, so a failed step loses
nothing. A state in which the :problem of the CRDT finds a problem isn't
pushed, since the other devices would leave it out, and the report has
:sync/malformed-state.

Run one step at a time, since a step that overlaps another can write an
older state after a newer one. What the user did during a step isn't in
its :state, so don't put the result in place of the state, but give it
to finish with the state as it is by then.

The backends need distinct names, since the cursors are kept by name,
and the same name twice throws.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close