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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3f3f1936db40a7ef5a4e74cd9d1f1e691440d385aaf1414b8bc0af99e8dd06d
4
- data.tar.gz: 44c4468e3fbce3c218562a9a9278242a5b6776317b9065fbc6a8a5d88341d0e3
3
+ metadata.gz: 57b6b2acc480ad0a0d40fb20c82a1091b30dbea74b674a5d02339dd1bf8b7f83
4
+ data.tar.gz: 2a36b774b1ad0693feef4194888a07b58e8899173de10bad52a174d012a6c2f4
5
5
  SHA512:
6
- metadata.gz: 06133afa987cb5d34990f8f0ba82bd7f55fb693c1f47aab9280d13f7c511acdb2b8d64b1b3bfc3e1cb4db0b4eeb948d93caefce28c4aba99c87666d4df94599e
7
- data.tar.gz: 2b8d25e17ca7a08c5e175a75718d8ba758f86db94fc19e58072c886419c308ff059e9fb2370d293201e2af8a98a12d16ca7f3f63d52f7c7c848c376e3d503431
6
+ metadata.gz: ab13771371259f4294fd94d207889643631f541dfdc26f43dbc6b2e7a454e79de4816dc4be560d1203b70dfe82a2e8b26fb3d1bcc56172743d2ce872060ee40f
7
+ data.tar.gz: 0a18c044a507454196599921a00ba0ace34fb7b3230dcb2c0a06124774f5893fa77d12a373f34d91cfe556af31a8c0d41a89571da8f3c0a37d8841dd141795e1
@@ -1,20 +1,18 @@
1
- require "collab/railtie"
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 "DocumentTransaction", "collab/models/document_transaction"
15
+ autoload "Commit", "collab/models/commit"
16
+ autoload "TrackedPosition", "collab/models/tracked_position"
19
17
  end
20
18
  end
@@ -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
- transactions = document.transactions
14
- .where("document_version > ?", starting_version)
15
- .order(document_version: :asc)
16
- .load
12
+ commits = @document.commits
13
+ .where("document_version > ?", starting_version)
14
+ .order(document_version: :asc)
15
+ .load
17
16
 
18
- raise "invalid version" unless transactions.first.document_version == (starting_version + 1) unless transactions.empty?
19
-
20
- transactions.lazy.map(&:as_json).each(method(:transmit))
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 submit(data)
24
- authorize_submit!(data)
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
@@ -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 :max_transactions, :application_job, :queue_document_transaction_job_as, :schema_package, :application_record, :document_transaction_model, :document_model, :channel_name
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
@@ -0,0 +1,6 @@
1
+ if defined?(Rails)
2
+ module Collab
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ 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, document: blank_document.dup)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Collab
2
- class Models::Base < ::Collab.config.application_record.constantize
2
+ class Models::Base < ::Collab.config.base_record.constantize
3
3
  self.abstract_class = true
4
4
  self.table_name_prefix = 'collab_'
5
5
  end
@@ -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
- validates :document, presence: true
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
- before_save :nullify_serialized_html, unless: :serialized_html_fresh?
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 - will be cached if possible (somewhat expensive)
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
- serialized_html = ::Collab::Bridge.current.document_to_html(self.document, schema_name: schema_name)
27
- self.update! serialized_html: serialized_html, serialized_html_version: document_version
28
- serialized_html
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 from_html(html)
32
- self.document = ::Collab::Bridge.current.html_to_document(html, schema_name: schema_name)
33
- end
31
+ def apply_commit(data)
32
+ base_version = data["v"]&.to_i
33
+ steps = data["steps"]
34
34
 
35
- def perform_transaction_later(data)
36
- ::Collab::DocumentTransactionJob.perform_later(@document, data)
37
- end
35
+ return false unless base_version
36
+ return false unless steps.is_a?(Array) && !steps.empty?
38
37
 
39
- def apply_transaction_now(data)
40
- return unless data["v"].is_a?(Integer)
38
+ self.with_lock do
39
+ return false unless self.possibly_saved_version? base_version
41
40
 
42
- return if (data["v"] - 1) != self.document_version # if expired between queue and perform, there is no need to aquire a lock. this is an optimization - not a guarantee
43
- with_lock do
44
- return if (data["v"] - 1) != self.document_version # now that we've aquired a lock to the record, ensure that we're still accessing the correct version
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
- self.document = transaction_result["doc"]
50
- self.document_version = data["v"]
51
- save!
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
- transactions.create! document_version: self.document_version, steps: data["steps"], ref: data["ref"]
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 as_json
58
- {id: id, document: document, version: self.document_version}
72
+ def from_html(html)
73
+ self.content = ::Collab::JS.html_to_document(html, schema_name: schema_name)
59
74
  end
60
75
 
61
- private
76
+ def as_json
77
+ {id: id, content: content, version: document_version}
78
+ end
62
79
 
63
- def nullify_serialized_html
80
+ def content_will_change!
81
+ super
64
82
  self.serialized_html = nil
65
83
  end
66
84
 
67
- def delete_old_transactions
68
- cutoff = document_version - ::Collab.config.max_transactions
69
- return if cutoff <= 0
70
- transactions.where("document_version < ?", cutoff).delete_all
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
@@ -1,3 +1,3 @@
1
1
  module Collab
2
- VERSION = '0.2.1'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -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
- raise "authorization failed"
18
+ # TODO: Replace with your own authorization logic
19
+ reject_unauthorized_connection
11
20
  end
12
21
  end
13
22
 
14
- # Called when a client submits a transaction in order to update a document
15
- # You should throw an error if unauthorized
16
- def authorize_submit!
17
- raise "authorization failed"
18
- end
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.max_transactions = 250
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]"} as subscription params to the Javascript client
13
- c.channel_name = "::CollabDocumentChannel"
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.application_record = "::ApplicationRecord"
27
- # If you want to use your own document model or document transaction model,
28
- c.document_model = "::Collab::Models::Document"
29
- c.document_transaction_model = "::Collab::Models::DocumentTransaction"
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.2.1
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-31 00:00:00.000000000 Z
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/document_transaction.rb
64
- - lib/collab/railtie.rb
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
@@ -1,7 +0,0 @@
1
- class Collab::DocumentTransactionJob < ::Collab.config.application_job.constantize
2
- queue_as ::Collab.config.queue_document_transaction_job_as
3
-
4
- def perform(document, data)
5
- document.apply_transaction_now(data)
6
- end
7
- end
@@ -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
@@ -1,4 +0,0 @@
1
- module Collab
2
- class Railtie < ::Rails::Railtie
3
- end
4
- end
@@ -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