collab 0.3.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: 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