collab 0.2.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|