collab 0.2.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/collab.rb +8 -10
- data/lib/collab/channel.rb +30 -15
- data/lib/collab/config.rb +29 -1
- data/lib/collab/engine.rb +6 -0
- data/lib/collab/has_collaborative_document.rb +1 -1
- data/lib/collab/has_tracked_document_positions.rb +34 -0
- data/lib/collab/js.rb +98 -0
- data/lib/collab/models/base.rb +1 -1
- data/lib/collab/models/commit.rb +27 -0
- data/lib/collab/models/document.rb +80 -43
- data/lib/collab/models/tracked_position.rb +45 -0
- data/lib/collab/selection.rb +21 -0
- data/lib/collab/version.rb +1 -1
- data/lib/generators/collab/install/install_generator.rb +3 -1
- data/lib/generators/collab/install/templates/channel.rb +17 -7
- data/lib/generators/collab/install/templates/create_collab_tables.rb.erb +41 -0
- data/lib/generators/collab/install/templates/initializer.rb +9 -19
- metadata +37 -7
- data/app/jobs/collab/document_transaction_job.rb +0 -7
- data/lib/collab/bridge.rb +0 -54
- data/lib/collab/models/document_transaction.rb +0 -22
- data/lib/collab/railtie.rb +0 -4
- data/lib/generators/collab/install/templates/create_collab_tables.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 57b6b2acc480ad0a0d40fb20c82a1091b30dbea74b674a5d02339dd1bf8b7f83
|
4
|
+
data.tar.gz: 2a36b774b1ad0693feef4194888a07b58e8899173de10bad52a174d012a6c2f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab13771371259f4294fd94d207889643631f541dfdc26f43dbc6b2e7a454e79de4816dc4be560d1203b70dfe82a2e8b26fb3d1bcc56172743d2ce872060ee40f
|
7
|
+
data.tar.gz: 0a18c044a507454196599921a00ba0ace34fb7b3230dcb2c0a06124774f5893fa77d12a373f34d91cfe556af31a8c0d41a89571da8f3c0a37d8841dd141795e1
|
data/lib/collab.rb
CHANGED
@@ -1,20 +1,18 @@
|
|
1
|
-
require "collab/
|
1
|
+
require "collab/version"
|
2
|
+
require "collab/config"
|
3
|
+
require "collab/js"
|
4
|
+
require "collab/engine"
|
2
5
|
|
3
6
|
module Collab
|
4
|
-
def self.config
|
5
|
-
@config ||= Collab::Config.new
|
6
|
-
return @config unless block_given?
|
7
|
-
yield @config
|
8
|
-
end
|
9
|
-
|
10
7
|
autoload "Channel", "collab/channel"
|
11
|
-
autoload "Config", "collab/config"
|
12
|
-
autoload "Bridge", "collab/bridge"
|
13
8
|
autoload "HasCollaborativeDocument", "collab/has_collaborative_document"
|
9
|
+
# autoload "DocumentSelection", "collab/selection"
|
10
|
+
autoload "HasTrackedDocumentPositions", "collab/has_tracked_document_positions"
|
14
11
|
|
15
12
|
module Models
|
16
13
|
autoload "Base", "collab/models/base"
|
17
14
|
autoload "Document", "collab/models/document"
|
18
|
-
autoload "
|
15
|
+
autoload "Commit", "collab/models/commit"
|
16
|
+
autoload "TrackedPosition", "collab/models/tracked_position"
|
19
17
|
end
|
20
18
|
end
|
data/lib/collab/channel.rb
CHANGED
@@ -1,30 +1,45 @@
|
|
1
1
|
module Collab
|
2
2
|
module Channel
|
3
|
-
def document; @document end
|
4
|
-
|
5
3
|
def subscribed
|
6
|
-
@document = find_document
|
7
|
-
|
4
|
+
reject_unauthorized_connection unless @document = find_document
|
5
|
+
|
8
6
|
starting_version = params[:startingVersion]&.to_i
|
9
7
|
raise "missing startingVersion" if starting_version.nil?
|
8
|
+
raise "invalid version" unless @document.possibly_saved_version? starting_version
|
10
9
|
|
11
|
-
stream_for document
|
10
|
+
stream_for @document
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
commits = @document.commits
|
13
|
+
.where("document_version > ?", starting_version)
|
14
|
+
.order(document_version: :asc)
|
15
|
+
.load
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
unless commits.empty?
|
18
|
+
raise "invalid version" unless commits.first.document_version == (starting_version + 1)
|
19
|
+
commits.lazy.map(&:as_json).each(method(:transmit))
|
20
|
+
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
|
25
|
-
document.perform_transaction_later(data)
|
23
|
+
def commit(data)
|
24
|
+
@document.apply_commit(data)
|
26
25
|
end
|
27
26
|
|
27
|
+
# def select(data)
|
28
|
+
# return unless defined?(_select)
|
29
|
+
|
30
|
+
# version = data["v"]&.to_i
|
31
|
+
# anchor_pos = data["anchor"]&.to_i
|
32
|
+
# head_pos = data["head"]&.to_i
|
33
|
+
|
34
|
+
# return unless version && @document.possibly_saved_version?(version) && anchor_pos && head_pos
|
35
|
+
|
36
|
+
# @document.resolve_selection(anchor_pos, head_pos, version: version) do |selection|
|
37
|
+
# _select selection
|
38
|
+
# end
|
39
|
+
|
40
|
+
# transmit({ack: "select"})
|
41
|
+
# end
|
42
|
+
|
28
43
|
def unsubscribed
|
29
44
|
stop_all_streams # this may not be needed
|
30
45
|
end
|
data/lib/collab/config.rb
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
module Collab
|
2
|
+
@config_mutex = Mutex.new
|
3
|
+
|
4
|
+
def self.config
|
5
|
+
if block_given?
|
6
|
+
@config_mutex.synchronize do
|
7
|
+
@config ||= ::Collab::Config.new
|
8
|
+
raise "[Collab] Tried to configure gem after first use" if @config.frozen?
|
9
|
+
yield @config
|
10
|
+
end
|
11
|
+
else
|
12
|
+
raise "[Collab] Missing configuration - Have you run `rails g collab:install` yet?" unless @config
|
13
|
+
@config.freeze # really weird stuff could happen if the config changes after first use, so freeze config
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
2
17
|
class Config
|
3
|
-
attr_accessor :
|
18
|
+
attr_accessor :base_record,
|
19
|
+
:channel,
|
20
|
+
:commit_model,
|
21
|
+
:document_model,
|
22
|
+
:max_commit_history_length,
|
23
|
+
:num_js_processes,
|
24
|
+
:schema_package,
|
25
|
+
:tracked_position_model
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
self.document_model = "Collab::Models::Document"
|
29
|
+
self.commit_model = "Collab::Models::Commit"
|
30
|
+
self.tracked_position_model = "Collab::Models::TrackedPosition"
|
31
|
+
end
|
4
32
|
end
|
5
33
|
end
|
@@ -8,7 +8,7 @@ module Collab
|
|
8
8
|
|
9
9
|
define_method attach_as do
|
10
10
|
super() || begin
|
11
|
-
document = self.__send__("build_#{attach_as}", schema_name: schema,
|
11
|
+
document = self.__send__("build_#{attach_as}", schema_name: schema, content: blank_document.dup)
|
12
12
|
document.save!
|
13
13
|
document
|
14
14
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Collab
|
2
|
+
module HasTrackedDocumentPositions
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def has_tracked_document_position(pos_name, optional: true)
|
7
|
+
has_one pos_name.to_sym, -> { where(name: pos_name) }, class_name: ::Collab.config.tracked_position_model, as: :owner, dependent: :destroy, autosave: true
|
8
|
+
validates pos_name.to_sym, presence: !optional
|
9
|
+
|
10
|
+
define_method :"#{pos_name}=" do |pos|
|
11
|
+
pos.name = pos_name
|
12
|
+
super(pos)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_tracked_document_selection(selection_name)
|
17
|
+
has_tracked_document_position :"#{selection_name}_anchor"
|
18
|
+
has_tracked_document_position :"#{selection_name}_head"
|
19
|
+
|
20
|
+
define_method selection_name do
|
21
|
+
anchor = self.send(:"#{selection_name}_anchor")
|
22
|
+
head = self.send(:"#{selection_name}_head")
|
23
|
+
|
24
|
+
::Collab::DocumentSelection.new anchor, head
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method :"#{selection_name}=" do |sel|
|
28
|
+
self.send(:"#{selection_name}_anchor=", sel&.anchor)
|
29
|
+
self.send(:"#{selection_name}_head=", sel&.head)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/collab/js.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Collab
|
4
|
+
module JS
|
5
|
+
@queue = Queue.new
|
6
|
+
@queue_initialized = false
|
7
|
+
@queue_initialization_mutex = Mutex.new
|
8
|
+
|
9
|
+
class <<self
|
10
|
+
def queue
|
11
|
+
initialize_queue unless @queue_initialized
|
12
|
+
@queue
|
13
|
+
end
|
14
|
+
|
15
|
+
# Calls the block given with a JS process acquired from the queue
|
16
|
+
# Will block until a JS process is available
|
17
|
+
def with_js
|
18
|
+
js = queue.pop
|
19
|
+
yield js
|
20
|
+
ensure
|
21
|
+
queue << js
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(name, data = nil, schema_name = nil)
|
25
|
+
req = {name: name, data: data, schemaPackage: ::Collab.config.schema_package}
|
26
|
+
req[:schemaName] = schema_name if schema_name
|
27
|
+
with_js { |js| js.call(JSON.generate(req)) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply_commit(document, commit, pos: nil, map_steps_through:, schema_name:)
|
31
|
+
call("applyCommit", {doc: document, commit: commit, mapStepsThrough: map_steps_through, pos: pos},schema_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def html_to_document(html, schema_name:)
|
35
|
+
call("htmlToDoc", html, schema_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def document_to_html(document, schema_name:)
|
39
|
+
call("docToHtml", document, schema_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def map_through(steps:, pos:)
|
43
|
+
call("mapThru", {steps: steps, pos: pos})
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
# Thread-safe initialization of the NodeJS process queue
|
48
|
+
def initialize_queue
|
49
|
+
@queue_initialization_mutex.synchronize do
|
50
|
+
unless @queue_initialized
|
51
|
+
::Collab.config.num_js_processes.times { @queue << ::Collab::JS::JSProcess.new }
|
52
|
+
@queue_initialized = true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class JSProcess
|
59
|
+
def initialize
|
60
|
+
@node = if defined?(Rails)
|
61
|
+
Dir.chdir(Rails.root) { open_node }
|
62
|
+
else
|
63
|
+
open_node
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def call(req)
|
68
|
+
@node.puts(req)
|
69
|
+
res = JSON.parse(@node.gets)
|
70
|
+
raise ::Collab::JS::JSRuntimeError.new(res["error"]) if res["error"]
|
71
|
+
res["result"]
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def open_node
|
76
|
+
IO.popen(["node", "-e", "require('@pmcp/authority/dist/rpc')"], "r+")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class JSRuntimeError < StandardError
|
81
|
+
def initialize(data)
|
82
|
+
@js_backtrace = data["stack"].split("\n").map{|f| "JavaScript #{f.strip}"} if data["stack"]
|
83
|
+
|
84
|
+
super(data["name"] + ": " + data["message"])
|
85
|
+
end
|
86
|
+
|
87
|
+
def backtrace
|
88
|
+
return unless val = super
|
89
|
+
|
90
|
+
if @js_backtrace
|
91
|
+
@js_backtrace + val
|
92
|
+
else
|
93
|
+
val
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/collab/models/base.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Collab
|
2
|
+
class Models::Commit < ::Collab::Models::Base
|
3
|
+
belongs_to :document, class_name: ::Collab.config.document_model
|
4
|
+
|
5
|
+
validates :steps, length: { in: 0..10, allow_nil: false }
|
6
|
+
validates :document_version, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
7
|
+
validates :ref, length: { maximum: 36 }, if: :ref
|
8
|
+
|
9
|
+
after_create_commit :broadcast
|
10
|
+
|
11
|
+
def steps
|
12
|
+
super || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def broadcast
|
16
|
+
::Collab.config.channel.constantize.broadcast_to(document, {
|
17
|
+
"v" => document_version,
|
18
|
+
"steps" => steps,
|
19
|
+
"ref" => ref
|
20
|
+
})
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.steps
|
24
|
+
pluck(:steps).flatten(1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,73 +1,110 @@
|
|
1
1
|
module Collab
|
2
2
|
class Models::Document < ::Collab::Models::Base
|
3
3
|
belongs_to :attached, polymorphic: true
|
4
|
-
has_many :transactions, class_name: ::Collab.config.document_transaction_model, foreign_key: :document_id
|
5
4
|
|
6
|
-
|
5
|
+
with_options foreign_key: :document_id do
|
6
|
+
has_many :commits, class_name: ::Collab.config.commit_model
|
7
|
+
has_many :tracked_positions, class_name: ::Collab.config.tracked_position_model
|
8
|
+
end
|
9
|
+
|
10
|
+
validates :content, presence: true
|
7
11
|
validates :document_version, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
8
12
|
validates :schema_name, presence: true
|
9
13
|
|
10
|
-
|
11
|
-
after_save :delete_old_transactions
|
12
|
-
|
13
|
-
# The already-serialized html version of this document
|
14
|
-
def serialized_html
|
15
|
-
return super if serialized_html_fresh?
|
16
|
-
end
|
17
|
-
|
18
|
-
def serialized_html_fresh?
|
19
|
-
serialized_html_version == document_version
|
20
|
-
end
|
14
|
+
after_save :delete_old_commits
|
21
15
|
|
22
|
-
# Serialize the document to html
|
16
|
+
# Serialize the document to html, uses cached if possible.
|
17
|
+
# Note that this may lock the document
|
23
18
|
def to_html
|
24
19
|
return serialized_html if serialized_html
|
25
20
|
|
26
|
-
|
27
|
-
self.
|
28
|
-
|
21
|
+
serialized_version = self.document_version
|
22
|
+
::Collab::JS.document_to_html(self.content, schema_name: schema_name).tap do |serialized_html|
|
23
|
+
Thread.new do # use a thread to prevent deadlocks and avoid incuring the cost of an inline-write
|
24
|
+
self.with_lock do
|
25
|
+
self.update_attribute(:serialized_html, serialized_html) if serialized_version == self.version and self.serialized_html.nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
31
|
+
def apply_commit(data)
|
32
|
+
base_version = data["v"]&.to_i
|
33
|
+
steps = data["steps"]
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
35
|
+
return false unless base_version
|
36
|
+
return false unless steps.is_a?(Array) && !steps.empty?
|
38
37
|
|
39
|
-
|
40
|
-
|
38
|
+
self.with_lock do
|
39
|
+
return false unless self.possibly_saved_version? base_version
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
transaction_result = ::Collab::Bridge.current.apply_transaction(self.document, data, schema_name: self.schema_name)
|
47
|
-
return unless transaction_result # check to make sure the transaction succeeded
|
41
|
+
self.document_version += 1
|
42
|
+
|
43
|
+
original_positions = self.tracked_positions.current.distinct.pluck(:pos, :assoc).map { |(pos, assoc)| {pos: pos, assoc: assoc} }
|
48
44
|
|
49
|
-
|
50
|
-
|
51
|
-
|
45
|
+
result = ::Collab::JS.apply_commit content,
|
46
|
+
{steps: steps},
|
47
|
+
map_steps_through: self.commits.where("document_version > ?", base_version).steps,
|
48
|
+
pos: original_positions,
|
49
|
+
schema_name: schema_name
|
52
50
|
|
53
|
-
|
51
|
+
self.content = result["doc"]
|
52
|
+
|
53
|
+
commits.create!({
|
54
|
+
steps: result["steps"],
|
55
|
+
ref: data["ref"],
|
56
|
+
document_version: self.document_version
|
57
|
+
})
|
58
|
+
|
59
|
+
self.save!
|
60
|
+
|
61
|
+
original_positions.lazy.zip(result["pos"]) do |original, res|
|
62
|
+
if res["deleted"]
|
63
|
+
tracked_positions.current.where(original).update_all deleted_at_version: self.document_version
|
64
|
+
elsif original[:pos] != res["pos"]
|
65
|
+
tracked_positions.current.where(original).update_all pos: res["pos"]
|
66
|
+
end
|
67
|
+
end
|
54
68
|
end
|
69
|
+
|
55
70
|
end
|
56
71
|
|
57
|
-
def
|
58
|
-
|
72
|
+
def from_html(html)
|
73
|
+
self.content = ::Collab::JS.html_to_document(html, schema_name: schema_name)
|
59
74
|
end
|
60
75
|
|
61
|
-
|
76
|
+
def as_json
|
77
|
+
{id: id, content: content, version: document_version}
|
78
|
+
end
|
62
79
|
|
63
|
-
def
|
80
|
+
def content_will_change!
|
81
|
+
super
|
64
82
|
self.serialized_html = nil
|
65
83
|
end
|
66
84
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
85
|
+
def possibly_saved_version?(version)
|
86
|
+
self.document_version >= version && self.oldest_saved_commit_version <= version
|
87
|
+
end
|
88
|
+
|
89
|
+
def oldest_saved_commit_version
|
90
|
+
v = document_version - ::Collab.config.max_commit_history_length
|
91
|
+
v > 0 ? v : 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def resolve_positions(*positions, **kwargs, &block)
|
95
|
+
::Collab.config.tracked_position_model.constantize.resolve(self, *positions, **kwargs, &block)
|
96
|
+
end
|
97
|
+
alias :resolve_position :resolve_positions
|
98
|
+
|
99
|
+
def resolve_selection(anchor_pos, head_pos, version:, &block)
|
100
|
+
::Collab::DocumentSelection.resolve(self, anchor_pos, head_pos, version: version, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def delete_old_commits
|
106
|
+
return if oldest_saved_commit_version == 0
|
107
|
+
commits.where("document_version < ?", oldest_saved_commit_version).delete_all
|
71
108
|
end
|
72
109
|
end
|
73
110
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Collab
|
2
|
+
# Represents a position in the document which is tracked between commits
|
3
|
+
#
|
4
|
+
# If the position is deleted through mapping, deleted_at_version will be set, the position will
|
5
|
+
# no longer be tracked,
|
6
|
+
#
|
7
|
+
class Models::TrackedPosition < ::Collab::Models::Base
|
8
|
+
belongs_to :document, class_name: ::Collab.config.document_model
|
9
|
+
belongs_to :owner, polymorphic: true
|
10
|
+
|
11
|
+
validates :pos, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
12
|
+
validates :assoc, presence: true, inclusion: {in: [-1, 1]}, numericality: {only_integer: true}
|
13
|
+
|
14
|
+
validates :deleted_at_version, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: :deleted_at_version
|
15
|
+
|
16
|
+
scope :current, -> { where(deleted_at_version: nil) }
|
17
|
+
|
18
|
+
# Resolves a set of positions and yields them to the block given, returning the result of the block
|
19
|
+
# The document will be locked IN SHARE MODE during resolution and the block execution
|
20
|
+
# Positions should consist of {"pos" => number, "assoc": number}
|
21
|
+
# Returns false if invalid version or any position has been deleted
|
22
|
+
def self.resolve(document, *positions, version:)
|
23
|
+
raise false unless document.possibly_saved_version? version
|
24
|
+
|
25
|
+
document.with_lock("FOR SHARE") do
|
26
|
+
unless document.document_version == version
|
27
|
+
steps = document.commits.where("document_version > ?", @mapped_to).order(document_version: :asc).pluck(:steps).flatten(1)
|
28
|
+
|
29
|
+
map_results = ::Collab::JS.map_through(steps: steps, pos: positions)["pos"]
|
30
|
+
|
31
|
+
map_results.each_with_index do |r, i|
|
32
|
+
return false if r["deleted"]
|
33
|
+
positions[i]["pos"] = r["pos"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
positions.map! do |p|
|
38
|
+
document.tracked_positions.current.find_or_initialize_by(pos: p["pos"], assoc: p["assoc"])
|
39
|
+
end
|
40
|
+
|
41
|
+
yield(*positions)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# module Collab
|
2
|
+
# class DocumentSelection
|
3
|
+
# attr_reader :anchor, :head
|
4
|
+
# def initialize(anchor, head)
|
5
|
+
# @anchor = anchor
|
6
|
+
# @head = head
|
7
|
+
# end
|
8
|
+
|
9
|
+
# def self.resolve(document, anchor_pos, head_pos, version:)
|
10
|
+
# anchor_assoc = anchor_pos > head_pos ? -1 : 1
|
11
|
+
|
12
|
+
# document.resolve_positions(
|
13
|
+
# {"pos" => anchor_pos, "assoc" => anchor_assoc},
|
14
|
+
# {"pos" => head_pos, "assoc" => anchor_assoc * -1},
|
15
|
+
# version: version
|
16
|
+
# ) do |anchor, head|
|
17
|
+
# yield new anchor, head
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
data/lib/collab/version.rb
CHANGED
@@ -17,8 +17,10 @@ module Collab
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def copy_files
|
20
|
+
@primary_key_type = Rails.application.config.generators.active_record[:primary_key_type]
|
21
|
+
|
20
22
|
migration_template(
|
21
|
-
"create_collab_tables.rb",
|
23
|
+
"create_collab_tables.rb.erb",
|
22
24
|
"db/migrate/create_collab_tables.rb",
|
23
25
|
)
|
24
26
|
|
@@ -1,19 +1,29 @@
|
|
1
1
|
class CollabDocumentChannel < ApplicationCable::Channel
|
2
2
|
include Collab::Channel
|
3
3
|
|
4
|
+
def commit(data)
|
5
|
+
if false # replace with your own authorization logic
|
6
|
+
raise "authorization not implemented"
|
7
|
+
end
|
8
|
+
|
9
|
+
super # make sure to call super in order to process the commit
|
10
|
+
end
|
11
|
+
|
4
12
|
private
|
5
13
|
|
6
14
|
# Find the document to subscribe to based on the params passed to the channel
|
7
|
-
# Authorization may also be performed here (raise an error)
|
15
|
+
# Authorization may also be performed here (raise an error to prevent subscription)
|
8
16
|
def find_document
|
9
17
|
Collab::Models::Document.find(params[:document_id]).tap do |document|
|
10
|
-
|
18
|
+
# TODO: Replace with your own authorization logic
|
19
|
+
reject_unauthorized_connection
|
11
20
|
end
|
12
21
|
end
|
13
22
|
|
14
|
-
#
|
15
|
-
# You
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
# Uncomment this line to receive the user's selection
|
24
|
+
# You must allow enable syncSelection on the client
|
25
|
+
#
|
26
|
+
# def _select(selection)
|
27
|
+
# ...
|
28
|
+
# end
|
19
29
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class CreateCollabTables < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :collab_documents<%= ", id: #{@primary_key_type.inspect}" if @primary_key_type %> do |t|
|
4
|
+
t.references :attached, null: false, index: false, polymorphic: true<%= ", type: #{@primary_key_type.inspect}" if @primary_key_type %>
|
5
|
+
t.string :attached_as, null: false
|
6
|
+
|
7
|
+
t.index [:attached_type, :attached_id, :attached_as], name: "index_collab_documents_on_attached"
|
8
|
+
|
9
|
+
t.jsonb :content, null: false
|
10
|
+
t.string :schema_name, null: false
|
11
|
+
t.integer :document_version, null: false, default: 0
|
12
|
+
t.text :serialized_html
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table :collab_commits, id: false do |t|
|
18
|
+
t.references :document, null: false, foreign_key: {to_table: :collab_documents}, index: false<%= ", type: #{@primary_key_type.inspect}" if @primary_key_type %>
|
19
|
+
t.integer :document_version, null: false
|
20
|
+
t.index [:document_id, :document_version], unique: true, order: {document_version: :asc}, name: "index_collab_commits"
|
21
|
+
|
22
|
+
t.jsonb :steps, array: true, null: false
|
23
|
+
t.string :ref
|
24
|
+
|
25
|
+
t.datetime :created_at, null: false
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table :collab_tracked_positions<%= ", id: #{@primary_key_type.inspect}" if @primary_key_type %> do |t|
|
29
|
+
t.references :document, null: false, foreign_key: {to_table: :collab_documents}, index: false<%= ", type: #{@primary_key_type.inspect}" if @primary_key_type %>
|
30
|
+
t.references :owner, null: false, polymorphic: true, index: false<%= ", type: #{@primary_key_type.inspect}" if @primary_key_type %>
|
31
|
+
t.string :name, null: false
|
32
|
+
|
33
|
+
t.integer :pos, null: false
|
34
|
+
t.integer :assoc, null: false, default: 1
|
35
|
+
t.integer :deleted_at_version
|
36
|
+
|
37
|
+
t.index [:document_id, :deleted_at_version, :pos], name: "index_collab_tracked_positions_on_document_pos"
|
38
|
+
t.index [:owner_type, :name, :owner_id], name: "index_collab_tracked_positions_on_owner"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -4,27 +4,17 @@ Collab.config do |c|
|
|
4
4
|
# To use a Git repo, see https://docs.npmjs.com/files/package.json#git-urls-as-dependencies
|
5
5
|
c.schema_package = "prosemirror-schema-basic"
|
6
6
|
# How many old transactions to keep per document
|
7
|
-
c.
|
7
|
+
c.max_commit_history_length = 250
|
8
|
+
# How many NodeJS child processes to run (shared among all threads)
|
9
|
+
c.num_js_processes = 3
|
8
10
|
|
9
|
-
# ActionCable settings
|
10
|
-
# ====================
|
11
11
|
# The document channel to use for collaboration
|
12
|
-
# If you change this, you must pass {channel: "[ChannelName]"}
|
13
|
-
c.
|
12
|
+
# If you change this, you must pass the value as {channel: "[ChannelName]"} in the params from the ActionCable client
|
13
|
+
c.channel = "CollabDocumentChannel"
|
14
14
|
|
15
|
-
# ActionJob settings
|
16
|
-
# ==================
|
17
|
-
# The base job class to use
|
18
|
-
c.application_job = "::ApplicationJob"
|
19
|
-
# The job queue to use for DocumentTransaction jobs
|
20
|
-
c.queue_document_transaction_job_as = :default
|
21
|
-
|
22
|
-
|
23
|
-
# ActiveRecord settings
|
24
|
-
# =====================
|
25
15
|
# The class which models in the gem should inherit from
|
26
|
-
c.
|
27
|
-
#
|
28
|
-
c.document_model = "
|
29
|
-
c.
|
16
|
+
c.base_record = "ApplicationRecord"
|
17
|
+
# The models to use, if you want to implement your own models
|
18
|
+
# c.document_model = "..."
|
19
|
+
# c.commit_model = "..."
|
30
20
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: collab
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Aubin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-07
|
11
|
+
date: 2020-08-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -44,6 +44,34 @@ dependencies:
|
|
44
44
|
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: webpacker
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: puma
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
47
75
|
description:
|
48
76
|
email:
|
49
77
|
- ben@benaubin.com
|
@@ -52,20 +80,22 @@ extensions: []
|
|
52
80
|
extra_rdoc_files: []
|
53
81
|
files:
|
54
82
|
- Rakefile
|
55
|
-
- app/jobs/collab/document_transaction_job.rb
|
56
83
|
- lib/collab.rb
|
57
|
-
- lib/collab/bridge.rb
|
58
84
|
- lib/collab/channel.rb
|
59
85
|
- lib/collab/config.rb
|
86
|
+
- lib/collab/engine.rb
|
60
87
|
- lib/collab/has_collaborative_document.rb
|
88
|
+
- lib/collab/has_tracked_document_positions.rb
|
89
|
+
- lib/collab/js.rb
|
61
90
|
- lib/collab/models/base.rb
|
91
|
+
- lib/collab/models/commit.rb
|
62
92
|
- lib/collab/models/document.rb
|
63
|
-
- lib/collab/models/
|
64
|
-
- lib/collab/
|
93
|
+
- lib/collab/models/tracked_position.rb
|
94
|
+
- lib/collab/selection.rb
|
65
95
|
- lib/collab/version.rb
|
66
96
|
- lib/generators/collab/install/install_generator.rb
|
67
97
|
- lib/generators/collab/install/templates/channel.rb
|
68
|
-
- lib/generators/collab/install/templates/create_collab_tables.rb
|
98
|
+
- lib/generators/collab/install/templates/create_collab_tables.rb.erb
|
69
99
|
- lib/generators/collab/install/templates/initializer.rb
|
70
100
|
- lib/tasks/collab_tasks.rake
|
71
101
|
homepage: https://github.com/benaubin/rails-collab
|
data/lib/collab/bridge.rb
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
require "collab/config"
|
2
|
-
require "json"
|
3
|
-
|
4
|
-
module Collab
|
5
|
-
class Bridge
|
6
|
-
class JSRuntimeError < StandardError
|
7
|
-
def initialize(data)
|
8
|
-
@js_backtrace = data["stack"].split("\n").map{|f| "JavaScript #{f.strip}"} if data["stack"]
|
9
|
-
|
10
|
-
super(data["name"] + ": " + data["message"])
|
11
|
-
end
|
12
|
-
|
13
|
-
def backtrace
|
14
|
-
return unless val = super
|
15
|
-
|
16
|
-
if @js_backtrace
|
17
|
-
@js_backtrace + val
|
18
|
-
else
|
19
|
-
val
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def initialize
|
25
|
-
@node = Dir.chdir(Rails.root) do
|
26
|
-
IO.popen(["node", "-e", "require('rails-collab-server')"], "r+")
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.current
|
31
|
-
@current ||= new
|
32
|
-
end
|
33
|
-
|
34
|
-
def call(name, data = nil, schema_name:)
|
35
|
-
req = {name: name, data: data, schemaPackage: ::Collab.config.schema_package, schemaName: schema_name}
|
36
|
-
@node.puts(JSON.generate(req))
|
37
|
-
res = JSON.parse(@node.gets)
|
38
|
-
raise ::Collab::Bridge::JSRuntimeError.new(res["error"]) if res["error"]
|
39
|
-
res["result"]
|
40
|
-
end
|
41
|
-
|
42
|
-
def apply_transaction(document, transaction, schema_name:)
|
43
|
-
call("applyTransaction", {doc: document, data: transaction}, schema_name: schema_name)
|
44
|
-
end
|
45
|
-
|
46
|
-
def html_to_document(html, schema_name:)
|
47
|
-
call("htmlToDoc", html, schema_name: schema_name)
|
48
|
-
end
|
49
|
-
|
50
|
-
def document_to_html(document, schema_name:)
|
51
|
-
call("docToHtml", document, schema_name: schema_name)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
module Collab
|
2
|
-
class Models::DocumentTransaction < ::Collab::Models::Base
|
3
|
-
belongs_to :document, class_name: ::Collab.config.document_model
|
4
|
-
|
5
|
-
validates :steps, presence: true
|
6
|
-
validates :document_version, presence: true
|
7
|
-
|
8
|
-
after_create_commit :broadcast
|
9
|
-
|
10
|
-
def as_json
|
11
|
-
{
|
12
|
-
v: document_version,
|
13
|
-
steps: steps,
|
14
|
-
ref: ref
|
15
|
-
}
|
16
|
-
end
|
17
|
-
|
18
|
-
def broadcast
|
19
|
-
::Collab.config.channel_name.constantize.broadcast_to(document, as_json)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
data/lib/collab/railtie.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
class CreateCollabTables < ActiveRecord::Migration[6.0]
|
2
|
-
def change
|
3
|
-
create_table :collab_documents, id: :uuid do |t|
|
4
|
-
t.references :attached, null: false, index: false, type: :uuid, polymorphic: true
|
5
|
-
t.string :attached_as
|
6
|
-
t.index [:attached_type, :attached_id, :attached_as], name: "index_collaborative_documents_on_attached"
|
7
|
-
|
8
|
-
t.string :schema_name, null: false
|
9
|
-
|
10
|
-
t.jsonb :document, null: false
|
11
|
-
t.integer :document_version, null: false, default: 0
|
12
|
-
|
13
|
-
t.text :serialized_html
|
14
|
-
t.integer :serialized_html_version
|
15
|
-
|
16
|
-
t.timestamps
|
17
|
-
end
|
18
|
-
|
19
|
-
create_table :collab_document_transactions, id: false do |t|
|
20
|
-
t.references :document, null: false, foreign_key: {to_table: :collab_documents}, type: :uuid, index: false
|
21
|
-
|
22
|
-
t.jsonb :steps, array: true, null: false
|
23
|
-
t.integer :document_version, null: false
|
24
|
-
|
25
|
-
t.string :ref
|
26
|
-
|
27
|
-
t.index [:document_id, :document_version], unique: true, order: {document_version: :asc}, name: "index_collaborative_document_transactions"
|
28
|
-
|
29
|
-
t.timestamps
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|