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 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