collab 0.3.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: f8c6f4ca46b51f7fd05bacdea192bbd5ec7979b6c44f7528665beda336fb6d43
4
- data.tar.gz: e2a0748fe7e1686a36a7ace934153433b1a499c18f8a0c930ed689564f9eb90e
3
+ metadata.gz: 57b6b2acc480ad0a0d40fb20c82a1091b30dbea74b674a5d02339dd1bf8b7f83
4
+ data.tar.gz: 2a36b774b1ad0693feef4194888a07b58e8899173de10bad52a174d012a6c2f4
5
5
  SHA512:
6
- metadata.gz: 36feda3ab2009c1a954434893a1ca89eed1f9492b08a20af7872d720abceaf4351398b16a9c6ba63cccf76f318ba387b3be29c85cf0616aba52a20f8fed7f71c
7
- data.tar.gz: bd79ab6de7c2c24185f581031d4b25f673d5ef037f2f3794d4b06cb2b2906c772227d8ad9aa3ec4b63b28d82f0a48f2fdd3a54f6835db919b67b628b0c83fdb0
6
+ metadata.gz: ab13771371259f4294fd94d207889643631f541dfdc26f43dbc6b2e7a454e79de4816dc4be560d1203b70dfe82a2e8b26fb3d1bcc56172743d2ce872060ee40f
7
+ data.tar.gz: 0a18c044a507454196599921a00ba0ace34fb7b3230dcb2c0a06124774f5893fa77d12a373f34d91cfe556af31a8c0d41a89571da8f3c0a37d8841dd141795e1
@@ -6,10 +6,13 @@ require "collab/engine"
6
6
  module Collab
7
7
  autoload "Channel", "collab/channel"
8
8
  autoload "HasCollaborativeDocument", "collab/has_collaborative_document"
9
+ # autoload "DocumentSelection", "collab/selection"
10
+ autoload "HasTrackedDocumentPositions", "collab/has_tracked_document_positions"
9
11
 
10
12
  module Models
11
13
  autoload "Base", "collab/models/base"
12
14
  autoload "Document", "collab/models/document"
13
15
  autoload "Commit", "collab/models/commit"
16
+ autoload "TrackedPosition", "collab/models/tracked_position"
14
17
  end
15
18
  end
@@ -1,19 +1,18 @@
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
- commits = document.commits
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
17
  unless commits.empty?
19
18
  raise "invalid version" unless commits.first.document_version == (starting_version + 1)
@@ -22,10 +21,25 @@ module Collab
22
21
  end
23
22
 
24
23
  def commit(data)
25
- authorize_commit!(data)
26
- document.commit_later(data)
24
+ @document.apply_commit(data)
27
25
  end
28
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
+
29
43
  def unsubscribed
30
44
  stop_all_streams # this may not be needed
31
45
  end
@@ -16,19 +16,18 @@ module Collab
16
16
 
17
17
  class Config
18
18
  attr_accessor :base_record,
19
- :base_job,
20
19
  :channel,
21
- :commit_job,
22
20
  :commit_model,
23
21
  :document_model,
24
22
  :max_commit_history_length,
25
23
  :num_js_processes,
26
- :schema_package
24
+ :schema_package,
25
+ :tracked_position_model
27
26
 
28
27
  def initialize
29
- self.commit_job = "Collab::CommitJob"
30
28
  self.document_model = "Collab::Models::Document"
31
29
  self.commit_model = "Collab::Models::Commit"
30
+ self.tracked_position_model = "Collab::Models::TrackedPosition"
32
31
  end
33
32
  end
34
33
  end
@@ -1,6 +1,6 @@
1
1
  if defined?(Rails)
2
2
  module Collab
3
- class Engine < ::Rails::Engine
4
- end
3
+ class Engine < ::Rails::Engine
4
+ end
5
5
  end
6
6
  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
@@ -21,20 +21,26 @@ module Collab
21
21
  queue << js
22
22
  end
23
23
 
24
- def call(name, data = nil, schema_name:)
25
- with_js { |js| js.call(name, *arguments, &block) }
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)) }
26
28
  end
27
29
 
28
- def apply_commit(document, commit, schema_name:)
29
- call("applyCommit", {doc: document, commit: commit}, schema_name: schema_name)
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)
30
32
  end
31
33
 
32
34
  def html_to_document(html, schema_name:)
33
- call("htmlToDoc", html, schema_name: schema_name)
35
+ call("htmlToDoc", html, schema_name)
34
36
  end
35
37
 
36
38
  def document_to_html(document, schema_name:)
37
- call("docToHtml", document, schema_name: schema_name)
39
+ call("docToHtml", document, schema_name)
40
+ end
41
+
42
+ def map_through(steps:, pos:)
43
+ call("mapThru", {steps: steps, pos: pos})
38
44
  end
39
45
 
40
46
  private
@@ -58,9 +64,8 @@ module Collab
58
64
  end
59
65
  end
60
66
 
61
- def call(name, data = nil, schema_name:)
62
- req = {name: name, data: data, schemaPackage: ::Collab.config.schema_package, schemaName: schema_name}
63
- @node.puts(JSON.generate(req))
67
+ def call(req)
68
+ @node.puts(req)
64
69
  res = JSON.parse(@node.gets)
65
70
  raise ::Collab::JS::JSRuntimeError.new(res["error"]) if res["error"]
66
71
  res["result"]
@@ -68,7 +73,7 @@ module Collab
68
73
 
69
74
  private
70
75
  def open_node
71
- IO.popen(["node", "-e", "require('rails-collab-server')"], "r+")
76
+ IO.popen(["node", "-e", "require('@pmcp/authority/dist/rpc')"], "r+")
72
77
  end
73
78
  end
74
79
 
@@ -2,52 +2,26 @@ module Collab
2
2
  class Models::Commit < ::Collab::Models::Base
3
3
  belongs_to :document, class_name: ::Collab.config.document_model
4
4
 
5
- validates :steps, presence: true
6
- validates :document_version, presence: true
7
- validates :ref, length: { maximum: 36 }
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
8
 
9
9
  after_create_commit :broadcast
10
10
 
11
- def self.from_json(data)
12
- new(document_version: data["v"]&.to_i, steps: data["steps"], ref: data["ref"])
11
+ def steps
12
+ super || []
13
13
  end
14
14
 
15
- def as_json
16
- {
17
- v: document_version,
18
- steps: steps,
19
- ref: ref
20
- }
21
- end
22
-
23
- def apply_later
24
- raise "cannot apply persisted commit" if self.persisted?
25
- raise "commit not valid" unless self.valid?
26
- return false if self.document.document_version != self.document_version
27
-
28
- ::Collab.config.commit_job.constantize.perform_later(self.document, as_json)
29
- end
30
-
31
- def apply!
32
- raise "cannot apply persisted commit" if self.persisted?
33
- raise "commit not valid" unless self.valid?
34
- return false if self.document.document_version != self.document_version # optimization, prevents need to apply lock if outdated
35
-
36
- self.document.with_lock do
37
- return false if self.document.document_version != self.document_version
38
-
39
- return false unless result = ::Collab::JS.apply_commit(self.document, self.to_json, schema_name: self.document.schema_name)
40
-
41
- self.document.document = result["doc"]
42
- self.document.document_version = self.document_version
43
-
44
- self.document.save!
45
- self.save!
46
- end
15
+ def broadcast
16
+ ::Collab.config.channel.constantize.broadcast_to(document, {
17
+ "v" => document_version,
18
+ "steps" => steps,
19
+ "ref" => ref
20
+ })
47
21
  end
48
22
 
49
- def broadcast
50
- ::Collab.config.channel_name.constantize.broadcast_to(document, as_json)
23
+ def self.steps
24
+ pluck(:steps).flatten(1)
51
25
  end
52
26
  end
53
27
  end
@@ -1,7 +1,11 @@
1
1
  module Collab
2
2
  class Models::Document < ::Collab::Models::Base
3
3
  belongs_to :attached, polymorphic: true
4
- has_many :commits, class_name: ::Collab.config.commit_model, foreign_key: :document_id
4
+
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
5
9
 
6
10
  validates :content, presence: true
7
11
  validates :document_version, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
@@ -24,12 +28,49 @@ module Collab
24
28
  end
25
29
  end
26
30
 
27
- def from_html(html)
28
- self.content = ::Collab::JS.html_to_document(html, schema_name: schema_name)
31
+ def apply_commit(data)
32
+ base_version = data["v"]&.to_i
33
+ steps = data["steps"]
34
+
35
+ return false unless base_version
36
+ return false unless steps.is_a?(Array) && !steps.empty?
37
+
38
+ self.with_lock do
39
+ return false unless self.possibly_saved_version? base_version
40
+
41
+ self.document_version += 1
42
+
43
+ original_positions = self.tracked_positions.current.distinct.pluck(:pos, :assoc).map { |(pos, assoc)| {pos: pos, assoc: assoc} }
44
+
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
50
+
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
68
+ end
69
+
29
70
  end
30
71
 
31
- def commit_later(data)
32
- commits.from_json(data).apply_later
72
+ def from_html(html)
73
+ self.content = ::Collab::JS.html_to_document(html, schema_name: schema_name)
33
74
  end
34
75
 
35
76
  def as_json
@@ -41,12 +82,29 @@ module Collab
41
82
  self.serialized_html = nil
42
83
  end
43
84
 
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
+
44
103
  private
45
104
 
46
105
  def delete_old_commits
47
- cutoff = document_version - ::Collab.config.max_commit_history_length
48
- return if cutoff <= 0
49
- commits.where("document_version < ?", cutoff).delete_all
106
+ return if oldest_saved_commit_version == 0
107
+ commits.where("document_version < ?", oldest_saved_commit_version).delete_all
50
108
  end
51
109
  end
52
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.3.1'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -1,6 +1,14 @@
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
@@ -8,15 +16,14 @@ class CollabDocumentChannel < ApplicationCable::Channel
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
11
- raise "authorization not implemented"
19
+ reject_unauthorized_connection
12
20
  end
13
21
  end
14
22
 
15
- # Called a commit is first received for processing
16
- # Throw an error to prevent the commit from being processed
17
- # You should consider adding some type of rate-limiting here
18
- def authorize_commit!(data)
19
- # TODO: Replace with your own authorization logic
20
- raise "authorization not implemented"
21
- 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
22
29
  end
@@ -24,5 +24,18 @@ class CreateCollabTables < ActiveRecord::Migration[6.0]
24
24
 
25
25
  t.datetime :created_at, null: false
26
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
27
40
  end
28
41
  end
@@ -12,11 +12,6 @@ Collab.config do |c|
12
12
  # If you change this, you must pass the value as {channel: "[ChannelName]"} in the params from the ActionCable client
13
13
  c.channel = "CollabDocumentChannel"
14
14
 
15
- # The class which jobs in the gem should inherit from
16
- c.base_job = "ApplicationJob"
17
- # The jobs to use, if you want to implement your own jobs
18
- # c.commit_job = "..."
19
-
20
15
  # The class which models in the gem should inherit from
21
16
  c.base_record = "ApplicationRecord"
22
17
  # The models to use, if you want to implement your own models
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.3.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-08-01 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,16 +80,18 @@ extensions: []
52
80
  extra_rdoc_files: []
53
81
  files:
54
82
  - Rakefile
55
- - app/jobs/collab/commit_job.rb
56
83
  - lib/collab.rb
57
84
  - lib/collab/channel.rb
58
85
  - lib/collab/config.rb
59
86
  - lib/collab/engine.rb
60
87
  - lib/collab/has_collaborative_document.rb
88
+ - lib/collab/has_tracked_document_positions.rb
61
89
  - lib/collab/js.rb
62
90
  - lib/collab/models/base.rb
63
91
  - lib/collab/models/commit.rb
64
92
  - lib/collab/models/document.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
@@ -1,7 +0,0 @@
1
- class Collab::CommitJob < ::Collab.config.base_job.constantize
2
- queue_as :default
3
-
4
- def perform(document, data)
5
- document.commits.from_json(data).apply!
6
- end
7
- end