[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/crdt c08e21d 06/18: Refactors and remote command
From: |
ELPA Syncer |
Subject: |
[elpa] externals/crdt c08e21d 06/18: Refactors and remote command |
Date: |
Thu, 9 Sep 2021 12:57:11 -0400 (EDT) |
branch: externals/crdt
commit c08e21d0dc77effffc0ef7a812e5ee2bb9df72f7
Author: Qiantan Hong <qhong@mit.edu>
Commit: Qiantan Hong <qhong@mit.edu>
Refactors and remote command
- change generic function interface crdt-process-message
- added crdt--readable-encode and crdt--readable-decode
- initial work for remote command
---
HACKING.org | 10 +++-
crdt.el | 189 ++++++++++++++++++++++++++++++++++++++++--------------------
2 files changed, 134 insertions(+), 65 deletions(-)
diff --git a/HACKING.org b/HACKING.org
index d04064b..f8e5b12 100644
--- a/HACKING.org
+++ b/HACKING.org
@@ -30,7 +30,8 @@ and second last two bytes represent site ID.
- =content= is the string to be inserted
+ delete ::
- body takes the form =(buffer-name position-hint (crdt-id . length)*)=
+ body takes the form =(buffer-name position-hint . crdt-id-list)=
+ - =crdt-id-list= is generated from =CRDT--DUMP-IDS= from the deleted text
- Peer State
+ cursor ::
@@ -130,6 +131,13 @@ and second last two bytes represent site ID.
+ overlay-remove ::
body takes the form =(buffer-name site-id logical-clock)=
+ - Remote Command
+ + command ::
+ body takes the form =(buffer-name site-id logical-clock command-symbol .
args)=
+
+ + return ::
+ body takes the form =(site-id logical-clock success-p . return-values)=
+
- Remote Buffer Process
+ process ::
body takes the form =(buffer-name string)=
diff --git a/crdt.el b/crdt.el
index cec2b21..ab71a1b 100644
--- a/crdt.el
+++ b/crdt.el
@@ -1024,7 +1024,7 @@ If POINT-CRDT-ID is NIL, remove the pseudo cursor and
region
overlays for this site.
The mark for that site is at MARK-CRDT-ID,
whose search starts around MARK-POSITION-HINT.
-If MARK-CRDT-ID, deactivate the pseudo region overlay."
+If MARK-CRDT-ID is NIL, deactivate the pseudo region overlay."
(when (and site-id (not (eq site-id (crdt--session-local-id crdt--session))))
(let ((ov-pair (gethash site-id crdt--pseudo-cursor-table)))
(if point-crdt-id
@@ -1144,14 +1144,7 @@ Verify that CRDT IDs in a document follows ascending
order."
Current buffer is assmuned to be the one with synchronization error."
(if (crdt--server-p)
(progn
- (let ((message (crdt--format-message `(error
,crdt--buffer-network-name ,@err))))
- (condition-case nil
- (read-from-string message)
- (invalid-read-syntax
- ;; (cdr err) must be unprintable, omit it for now
- ;; maybe handle some objects in the future
- ;; (e.g. represent buffer object with its name)
- (setq message (crdt--format-message `(error
,crdt--buffer-network-name ,(car err))))))
+ (let ((message (crdt--format-message `(error
,crdt--buffer-network-name ,(car err) ,(crdt--readable-encode (cdr err))))))
(process-send-string crdt--process message)))
(ding)
(read-only-mode)
@@ -1168,6 +1161,30 @@ Return the string."
(print-length nil))
(prin1-to-string args)))
+(defun crdt--readable-encode (object)
+ "Return an object ``similar'' to OBJECT at best effort,
+but whose printed representation can be read back.
+The symbol CRDT-EVAL is used as an special marker in the encoding
+and the behavior is undefined if OBJECT itself contains this symbol."
+ (cl-typecase object
+ ((or symbol string number character) object)
+ (cons (cons (crdt--ensure-readable (car object)) (crdt--ensure-readable
(cdr object))))
+ (buffer (list 'crdt-eval 'buffer
+ (buffer-local-value 'crdt--buffer-network-name buffer)))
+ (t (list 'crdt-eval 'unreadable
+ (prin1-to-string object)))))
+
+(defun crdt--readable-decode (object)
+ "Reconstruct the original object from
+CRDT--READABLE-ENCODEd OBJECT at best effort."
+ (if (consp object)
+ (if (eq (car object) 'crdt-eval)
+ (cl-case (cadr object)
+ ((buffer) (crdt--with-buffer-name (caddr object) (current-buffer)))
+ ((unreadable) (caddr object)))
+ object)
+ object))
+
(cl-defun crdt--broadcast-maybe (message-string &optional (without t))
"Broadcast or send MESSAGE-STRING.
If (CRDT--SESSION-NETWORK-PROCESS CRDT--SESSION) is a server process,
@@ -1307,30 +1324,36 @@ The network process for the client connection is
PROCESS."
,(crdt--session-focused-buffer-name
crdt--session))))
(let ((contact-message `(contact ,client-id ,(process-get process
'client-name)
,(process-contact process :host)
- ,(process-contact process :service))))
- (crdt-process-message contact-message process)))))
+ ,(process-contact process :service)))
+ (crdt--process process))
+ (crdt-process-message-1 contact-message)))))
+
+(cl-defgeneric crdt-process-message (message string) "Handle MESSAGE read from
STRING.
+We include STRING argument so that we don't need to convert MESSAGE to string
again
+when we need to broadcast it.")
-(cl-defgeneric crdt-process-message (message process) "Handle MESSAGE received
from PROCESS.")
+(defun crdt-process-message-1 (message)
+ (crdt-process-message message (crdt--format-message message)))
-(cl-defmethod crdt-process-message (message process)
+(cl-defmethod crdt-process-message (message _string)
(message "Unrecognized message %S from %s:%s."
- message (process-contact process :host) (process-contact process
:service))
+ message (process-contact crdt--process :host) (process-contact
crdt--process :service))
(signal 'crdt-unrecognized-message nil))
-(cl-defmethod crdt-process-message ((message (head insert)) process)
+(cl-defmethod crdt-process-message ((message (head insert)) string)
(cl-destructuring-bind (buffer-name crdt-id position-hint content) (cdr
message)
(crdt--with-buffer-name buffer-name
(crdt--remote-insert (base64-decode-string crdt-id) position-hint
content)))
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
-(cl-defmethod crdt-process-message ((message (head delete)) process)
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id))
+(cl-defmethod crdt-process-message ((message (head delete)) string)
(cl-destructuring-bind (buffer-name position-hint . id-base64-pairs) (cdr
message)
(mapc (lambda (p) (rplaca (cdr p) (base64-decode-string (cadr p))))
id-base64-pairs)
(crdt--with-buffer-name buffer-name
- (crdt--remote-delete position-hint id-base64-pairs))))
+ (crdt--remote-delete position-hint id-base64-pairs)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
-(cl-defmethod crdt-process-message ((message (head cursor)) process)
+(cl-defmethod crdt-process-message ((message (head cursor)) string)
(cl-destructuring-bind (buffer-name site-id point-position-hint point-crdt-id
mark-position-hint mark-crdt-id)
(cdr message)
@@ -1339,16 +1362,16 @@ The network process for the client connection is
PROCESS."
(and point-crdt-id (base64-decode-string
point-crdt-id))
mark-position-hint
(and mark-crdt-id (base64-decode-string
mark-crdt-id)))))
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
-(cl-defmethod crdt-process-message ((message (head get)) process)
+(cl-defmethod crdt-process-message ((message (head get)) _string)
(cl-destructuring-bind (buffer-name) (cdr message)
(let ((buffer (gethash buffer-name (crdt--session-buffer-table
crdt--session))))
(if (and buffer (buffer-live-p buffer))
- (crdt--sync-buffer-to-client buffer process)
- (process-send-string process (crdt--format-message `(remove
,buffer-name)))))))
+ (crdt--sync-buffer-to-client buffer crdt--process)
+ (process-send-string crdt--process (crdt--format-message `(remove
,buffer-name)))))))
-(cl-defmethod crdt-process-message ((message (head sync)) _process)
+(cl-defmethod crdt-process-message ((message (head sync)) _string)
(unless (crdt--server-p) ; server shouldn't receive this
(cl-destructuring-bind (buffer-name . ids) (cdr message)
(crdt--with-buffer-name buffer-name
@@ -1367,7 +1390,7 @@ The network process for the client connection is PROCESS."
(crdt--load-ids ids))))
(crdt--refresh-buffers-maybe)))
-(cl-defmethod crdt-process-message ((message (head ready)) _process)
+(cl-defmethod crdt-process-message ((message (head ready)) _string)
(unless (crdt--server-p) ; server shouldn't receive this
(cl-destructuring-bind (buffer-name mode) (cdr message)
(crdt--with-buffer-name buffer-name
@@ -1380,20 +1403,20 @@ The network process for the client connection is
PROCESS."
(funcall crdt--buffer-sync-callback)
(setq crdt--buffer-sync-callback nil))))))
-(cl-defmethod crdt-process-message ((message (head error)) _process)
+(cl-defmethod crdt-process-message ((message (head error)) _string)
(unless (crdt--server-p)
(cl-destructuring-bind (buffer-name &rest err) (cdr message)
(crdt--with-buffer-name buffer-name
(message "Server side error %s." err)
(crdt--recover)))))
-(cl-defmethod crdt-process-message ((message (head add)) _process)
+(cl-defmethod crdt-process-message ((message (head add)) _string)
(dolist (buffer-name (cdr message))
(unless (gethash buffer-name (crdt--session-buffer-table crdt--session))
(puthash buffer-name nil (crdt--session-buffer-table crdt--session)))
(crdt--refresh-buffers-maybe)))
-(cl-defmethod crdt-process-message ((message (head remove)) process)
+(cl-defmethod crdt-process-message ((message (head remove)) string)
(let ((saved-session crdt--session))
(dolist (buffer-name (cdr message))
(let ((buffer (gethash buffer-name (crdt--session-buffer-table
crdt--session))))
@@ -1406,24 +1429,25 @@ The network process for the client connection is
PROCESS."
(message "Server stopped sharing %s."
(mapconcat #'identity (cdr message) ", "))
(let ((crdt--session saved-session))
- (crdt--broadcast-maybe (crdt--format-message message)
- (when process (process-get process 'client-id)))
+ (crdt--broadcast-maybe string
+ (when crdt--process
+ (process-get crdt--process 'client-id)))
(crdt--refresh-buffers-maybe))))
-(cl-defmethod crdt-process-message ((message (head login)) process)
+(cl-defmethod crdt-process-message ((message (head login)) _string)
(cl-destructuring-bind (id session-name) (cdr message)
(puthash 0 (crdt--make-contact-metadata nil nil
- (process-contact process :host)
- (process-contact process :service))
+ (process-contact crdt--process
:host)
+ (process-contact crdt--process
:service))
(crdt--session-contact-table crdt--session))
(setf (crdt--session-name crdt--session) (concat session-name "@"
(crdt--session-name crdt--session)))
(setf (crdt--session-local-id crdt--session) id)
(crdt--refresh-sessions-maybe)))
-(cl-defmethod crdt-process-message ((_message (head leave)) process)
- (delete-process process))
+(cl-defmethod crdt-process-message ((_message (head leave)) _string)
+ (delete-process crdt--process))
-(cl-defmethod crdt-process-message ((message (head challenge)) _process)
+(cl-defmethod crdt-process-message ((message (head challenge)) _string)
(unless (crdt--server-p) ; server shouldn't receive this
(message nil)
(let ((password (read-passwd
@@ -1434,7 +1458,7 @@ The network process for the client connection is PROCESS."
`(hello ,(crdt--session-local-name crdt--session)
,(gnutls-hash-mac 'SHA1 password (cadr
message))))))))
-(cl-defmethod crdt-process-message ((message (head contact)) process)
+(cl-defmethod crdt-process-message ((message (head contact)) string)
(cl-destructuring-bind
(site-id display-name &optional host service) (cdr message)
(if display-name
@@ -1446,9 +1470,9 @@ The network process for the client connection is PROCESS."
(setf (crdt--contact-metadata-display-name existing-item)
display-name)))
(remhash site-id (crdt--session-contact-table crdt--session)))
(crdt--refresh-users-maybe))
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
-(cl-defmethod crdt-process-message ((message (head focus)) process)
+(cl-defmethod crdt-process-message ((message (head focus)) string)
(cl-destructuring-bind
(site-id buffer-name) (cdr message)
(let ((existing-item (gethash site-id (crdt--session-contact-table
crdt--session))))
@@ -1457,7 +1481,7 @@ The network process for the client connection is PROCESS."
;; (setq crdt--focused-buffer-name buffer-name)
;; (switch-to-buffer (gethash buffer-name (crdt--session-buffer-table
crdt--session))))
(crdt--refresh-users-maybe))
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
(define-error 'crdt-unrecognized-message "Unhandled
crdt-unrecognized-message.")
@@ -1477,15 +1501,17 @@ Handle received STRING from PROCESS."
(insert string)
(set-marker (process-mark process) (point))
(goto-char (point-min))
- (let (message)
- (while (setq message (ignore-errors (read (current-buffer))))
+ (let (message string start)
+ (while (setq start (point)
+ message (ignore-errors (read (current-buffer))))
(when crdt--log-network-traffic
(print message))
+ (setq string (buffer-substring-no-properties start (point)))
(condition-case err
(if (or (not (crdt--server-p)) (process-get process
'authenticated))
(let ((crdt--inhibit-update t)
(crdt--process process))
- (crdt-process-message message process))
+ (crdt-process-message message string))
(cl-block nil
(when (eq (car message) 'hello)
(cl-destructuring-bind (name &optional response) (cdr
message)
@@ -1500,7 +1526,7 @@ Handle received STRING from PROCESS."
(gnutls-hash-mac 'SHA1 (substring
(process-get process 'password)) challenge))
(process-send-string process (crdt--format-message
`(challenge ,challenge))))))
(error
- (message "%s error when processing message from %s:%s,
disconnecting." err
+ (message "%s error when processing message %s from %s:%s,
disconnecting." err message
(process-contact process :host) (process-contact process
:service))
(if (crdt--server-p)
(delete-process process)
@@ -1518,12 +1544,12 @@ Handle received STRING from PROCESS."
;; generate a clear cursor message and a clear contact message
(let* ((client-id (process-get client 'client-id))
(clear-contact-message `(contact ,client-id nil)))
- (crdt-process-message clear-contact-message client)
+ (let ((crdt--process client))
+ (crdt-process-message-1 clear-contact-message))
(maphash
(lambda (k _)
- (crdt-process-message
- `(cursor ,k ,client-id 1 nil 1 nil)
- client))
+ (let ((crdt--process client))
+ (crdt-process-message-1 `(cursor ,k ,client-id 1 nil 1 nil))))
(crdt--session-buffer-table crdt--session))
(crdt--refresh-users-maybe))
(when (process-buffer client) (kill-buffer (process-buffer client))))))
@@ -1622,7 +1648,7 @@ of the current buffer."
(if (crdt--server-p)
(let ((buffer-name crdt--buffer-network-name))
(let ((remove-message `(remove ,buffer-name)))
- (crdt-process-message remove-message nil)))
+ (crdt-process-message-1 remove-message)))
(message "Only server can stop sharing a buffer."))
(message "Not a CRDT shared buffer.")))
@@ -1896,7 +1922,7 @@ Join with DISPLAY-NAME."
(cl-incf (crdt--session-local-clock crdt--session))))
new-overlay))
-(cl-defmethod crdt-process-message ((message (head overlay-add)) process)
+(cl-defmethod crdt-process-message ((message (head overlay-add)) string)
(cl-destructuring-bind
(buffer-name site-id logical-clock species
front-advance rear-advance start-hint start-id-base64
end-hint end-id-base64)
@@ -1914,7 +1940,7 @@ Join with DISPLAY-NAME."
(let ((crdt--inhibit-overlay-advices t)
(crdt--modifying-overlay-metadata t))
(overlay-put new-overlay 'crdt-meta meta)))))
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
(defun crdt--move-overlay-advice (orig-fun ov beg end &rest args)
(when crdt-mode
@@ -1950,7 +1976,7 @@ Join with DISPLAY-NAME."
(end (crdt--find-id (base64-decode-string end-id-base64)
end-hint rear-advance)))
(let ((crdt--inhibit-overlay-advices t))
(move-overlay ov start end)))))))
- (crdt--broadcast-maybe (crdt--format-message message) nil))
+ (crdt--broadcast-maybe string nil))
(defun crdt--delete-overlay-advice (orig-fun ov)
(unless crdt--inhibit-overlay-advices
@@ -1963,7 +1989,7 @@ Join with DISPLAY-NAME."
`(overlay-remove
,crdt--buffer-network-name ,(car key) ,(cdr key)))))))))
(funcall orig-fun ov))
-(cl-defmethod crdt-process-message ((message (head overlay-remove)) process)
+(cl-defmethod crdt-process-message ((message (head overlay-remove)) string)
(cl-destructuring-bind (buffer-name site-id logical-clock) (cdr message)
(crdt--with-buffer-name buffer-name
(let* ((key (cons site-id logical-clock))
@@ -1972,7 +1998,7 @@ Join with DISPLAY-NAME."
(remhash key crdt--overlay-table)
(let ((crdt--inhibit-overlay-advices t))
(delete-overlay ov))))))
- (crdt--broadcast-maybe (crdt--format-message message) (process-get process
'client-id)))
+ (crdt--broadcast-maybe string (process-get crdt--process 'client-id)))
(defun crdt--overlay-put-advice (orig-fun ov prop value)
(unless (and (eq prop 'crdt-meta)
@@ -1984,16 +2010,13 @@ Join with DISPLAY-NAME."
(setf (crdt--overlay-metadata-plist meta) (plist-put
(crdt--overlay-metadata-plist meta) prop value))
(let* ((key (crdt--overlay-metadata-lamport-timestamp meta))
(message (crdt--format-message `(overlay-put
,crdt--buffer-network-name
- ,(car key)
,(cdr key) ,prop ,value))))
- (condition-case nil
- (progn ; filter non-readable object
- (read-from-string message)
- (crdt--broadcast-maybe message))
- (invalid-read-syntax)))))))
+ ,(car key)
,(cdr key) ,prop ,(crdt--readable-encode value)))))
+ (crdt--broadcast-maybe message))))))
(funcall orig-fun ov prop value)))
-(cl-defmethod crdt-process-message ((message (head overlay-put)) _process)
+(cl-defmethod crdt-process-message ((message (head overlay-put)) string)
(cl-destructuring-bind (buffer-name site-id logical-clock prop value) (cdr
message)
+ (setq value (crdt--readable-decode value))
(crdt--with-buffer-name buffer-name
(let ((ov (gethash (cons site-id logical-clock) crdt--overlay-table)))
(when ov
@@ -2003,7 +2026,7 @@ Join with DISPLAY-NAME."
(when (memq (crdt--overlay-metadata-species meta)
crdt--enabled-overlay-species)
(let ((crdt--inhibit-overlay-advices t))
(overlay-put ov prop value))))))))
- (crdt--broadcast-maybe (crdt--format-message message) nil))
+ (crdt--broadcast-maybe string nil))
(advice-add 'make-overlay :around #'crdt--make-overlay-advice)
@@ -2040,7 +2063,45 @@ Join with DISPLAY-NAME."
(cl-loop for command in '(org-cycle org-shifttab)
do (advice-add command :around #'crdt--org-overlay-advice))
+;;; Remote Command
+
+(cl-defmethod crdt-process-message ((message (head command)) _string)
+ (cl-destructuring-bind (buffer-name site-id logical-clock command-symbol
&rest args) (cdr message)
+ (crdt--with-buffer-name buffer-name
+ (save-excursion
+ (goto-char (overlay-start (car (gethash site-id
crdt--pseudo-cursor-table))))
+ (let ((return-message
+ (if (get command-symbol 'crdt-allow-remote-call)
+ (condition-case err
+ (list t (apply command-symbol (mapcar
#'crdt--readable-decode args)))
+ (error (list nil (car err) (crdt--readable-encode (cdr
err)))))
+ (list nil 'crdt-access-denied))))
+ (process-send-string crdt--process (crdt--format-message `(return
,site-id ,logical-clock ,@return-message))))))))
+
+(cl-defmethod crdt-process-message ((message (head return)) _string)
+ nil)
+
+(defun crdt-make-remote-command-advice (function-symbol)
+ (lambda (orig-fun &rest args)
+ (if (and crdt--session (not (crdt--server-p)))
+ (process-send-string (crdt--session-network-process crdt--session)
+ (crdt--format-message
+ `(command ,crdt--buffer-network-name
+ ,(crdt--session-local-id crdt--session)
+ ,(crdt--session-local-clock
crdt--session)
+ ,function-symbol ,@args)))
+ (apply orig-fun args))))
+
+(defun crdt-register-remote-command (command-symbol)
+ (put command-symbol 'crdt-allow-remote-call t)
+ (advice-add command-symbol :around (crdt-make-remote-command-advice
command-symbol) '((name . crdt-remote-command-advice))))
+
+(defun crdt-unregister-remote-command (command-symbol)
+ (cl-remprop command-symbol 'crdt-allow-remote-call)
+ (advice-remove command-symbol 'crdt-remote-command-advice))
+
;;; pseudo process
+
(cl-defstruct (crdt--pseudo-process (:constructor crdt--make-pseudo-process))
buffer
mark)
@@ -2083,7 +2144,7 @@ Join with DISPLAY-NAME."
process
(funcall orig-func process)))
-(cl-defmethod crdt-process-message ((message (head process-mark)) _process)
+(cl-defmethod crdt-process-message ((message (head process-mark)) _string)
(cl-destructuring-bind (buffer-name crdt-id position-hint) (cdr message)
(crdt--with-buffer-name buffer-name
(save-excursion
- [elpa] externals/crdt f9a7ec9 01/18: update header, (continued)
- [elpa] externals/crdt f9a7ec9 01/18: update header, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt dcf1dd5 02/18: Update version number, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt 605d8fa 07/18: initial experiment of xscheme.el integration, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt b6bcc8b 03/18: Merge branch 'master' into development, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt cc37dcd 04/18: add description of bridge protocol, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt bf1dc5d 09/18: typo, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt e4493e5 13/18: bug fix for comint integration, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt 52cbf50 15/18: autoload; fix bug for cmuscheme, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt 3f6f566 11/18: lots of changes, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt 38476da 05/18: Merge branch 'fix' into development, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt c08e21d 06/18: Refactors and remote command,
ELPA Syncer <=
- [elpa] externals/crdt 936418c 14/18: Synchronize text property on changes, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt e175d65 08/18: Remote command, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt 6b85d8a 10/18: refactor, replace cl-generic with hashtable of handlers, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt d7bc982 12/18: update README, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt 40bd5d4 16/18: document, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt b11bbc0 17/18: some more autoloads, ELPA Syncer, 2021/09/09
- [elpa] externals/crdt c1378c5 18/18: Merge branch 'development', ELPA Syncer, 2021/09/09