collab 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d67b7d2a30faff8c19e2ef8ebbacf175bc716c8eec5b4a3672bc088554067759
4
+ data.tar.gz: 413ba76d5f6d46f390ff213b85369be667801a140151d91b166c5bc4dd1b0eff
5
+ SHA512:
6
+ metadata.gz: e11fada08ea6e80d32d3d02ead738495ad0c89d044065563c543fbd68c4c0425c4e04f7475306dd69751de6f081637f99e6919b9d42b35ffd021882c3dc4d814
7
+ data.tar.gz: e7dab34c2530ba90fe8dee20e10cca6db76bea46d37205c703186c40c93cd6c7d9d0b92ef1a149e85060c1057f61f3a08d055d8b2f0389bdcd6f60445b646f01
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Collab'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,28 @@
1
+ class Collab::DocumentChannel < ApplicationCable::Channel
2
+ def subscribed
3
+ @document = instance_exec(::Collab.config.find_document_for_subscribe)
4
+
5
+ starting_version = params[:startingVersion]&.to_i
6
+ raise "missing startingVersion" if starting_version.nil?
7
+
8
+ stream_for @document
9
+
10
+ transactions = @document.transactions
11
+ .where("document_version > ?", starting_version)
12
+ .order(document_version: :asc)
13
+ .load
14
+
15
+ raise "invalid version" unless transactions.first.document_version == (starting_version + 1) unless transactions.empty?
16
+
17
+ transactions.lazy.map(&:as_json).each(method(:transmit))
18
+ end
19
+
20
+ def submit(data)
21
+ instance_exec(@document, data, &::Collab.config.authorize_update_document)
22
+ @document.perform_transaction_later(data)
23
+ end
24
+
25
+ def unsubscribed
26
+ stop_all_streams # this may not be needed
27
+ end
28
+ end
@@ -0,0 +1,7 @@
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
@@ -0,0 +1,19 @@
1
+ require "collab/config"
2
+ require "collab/railtie"
3
+
4
+ module Collab
5
+ def config
6
+ @config ||= Collab::Config.new
7
+ return @config unless block_given?
8
+ yield @config
9
+ end
10
+
11
+ autoload "Bridge", "collab/bridge"
12
+ autoload "HasCollaborativeDocument", "collab/has_collaborative_document"
13
+
14
+ module Models
15
+ autoload "Base", "collab/models/base"
16
+ autoload "Document", "collab/models/document"
17
+ autoload "DocumentTransaction", "collab/models/document_transaction"
18
+ end
19
+ end
@@ -0,0 +1,54 @@
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", %q{"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, schemaName:)
35
+ req = {name: name, data: data, schemaPackage: ::Collab.config.schema_package, schemaName: schemaName}
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, schemaName:)
43
+ call("applyTransaction", {doc: document, data: transaction}, schemaName: schemaName)
44
+ end
45
+
46
+ def html_to_document(html, schemaName:)
47
+ call("htmlToDoc", html, schemaName: schemaName)
48
+ end
49
+
50
+ def document_to_html(document, schemaName:)
51
+ call("docToHtml", document, schemaName: schemaName)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,15 @@
1
+ module Collab
2
+ class Config
3
+ attr_accessor :max_transactions, :application_job, :queue_document_transaction_job_as, :schema_package, :application_record, :document_transaction_model, :document_model
4
+
5
+ def find_document_for_subscribe(&block)
6
+ @find_document_for_subscribe unless block
7
+ @find_document_for_subscribe = block
8
+ end
9
+
10
+ def authorize_update_document(&block)
11
+ @authorize_update_document unless block
12
+ @authorize_update_document = block
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Collab
2
+ module HasCollaborativeDocument
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def has_collaborative_document(attach_as, schema:, blank_document:)
7
+ has_one attach_as, -> { where(attached_as: attach_as) }, class_name: ::Collab.config.document_model, as: :attached
8
+
9
+ define_method attach_as do
10
+ super() || begin
11
+ document = self.__send__("build_#{attach_as}", schema_name: schema, document: blank_document.dup)
12
+ document.save!
13
+ document
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module Collab
2
+ class Models::Base < ::Collab.config.application_record.constantize
3
+ self.abstract_class = true
4
+ self.table_name_prefix = 'collab_'
5
+ end
6
+ end
@@ -0,0 +1,75 @@
1
+ module Collab
2
+ class Models::Document < ::Collab::Models::Base
3
+ belongs_to :attached, polymorphic: true
4
+ has_many :transactions, class_name: ::Collab.config.document_transaction_model, foreign_key: :document_id
5
+
6
+ validates :document, presence: true
7
+ validates :document_version, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
8
+ validates :schema_name, presence: true
9
+
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
21
+
22
+ # Serialize the document to html - will be cached if possible (somewhat expensive)
23
+ def to_html
24
+ return serialized_html if serialized_html
25
+
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
29
+ end
30
+
31
+ def from_html(html)
32
+ raise "cannot override a persisted document" if self.persisted?
33
+
34
+ self.document = ::Collab::Bridge.current.html_to_document(html, schema_name: schema_name)
35
+ end
36
+
37
+ def perform_transaction_later(data)
38
+ ::Collab::DocumentTransactionJob.perform_later(@document, data)
39
+ end
40
+
41
+ def apply_transaction_now(data)
42
+ return unless data["v"].is_a?(Integer)
43
+
44
+ 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
45
+ with_lock do
46
+ 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
47
+
48
+ transaction_result = ::Collab::Bridge.current.apply_transaction(self.document, data, schema_name: self.schema_name)
49
+ return unless transaction_result # check to make sure the transaction succeeded
50
+
51
+ self.document = transaction_result["doc"]
52
+ self.document_version = data["v"]
53
+ save!
54
+
55
+ transactions.create! document_version: self.document_version, steps: data["steps"], ref: data["ref"]
56
+ end
57
+ end
58
+
59
+ def as_json
60
+ {id: id, document: document, version: self.document_version}
61
+ end
62
+
63
+ private
64
+
65
+ def nullify_serialized_html
66
+ self.serialized_html = nil
67
+ end
68
+
69
+ def delete_old_transactions
70
+ cutoff = document_version - ::Collab.config.max_transactions
71
+ return if cutoff <= 0
72
+ transactions.where("document_version < ?", cutoff).delete_all
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
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::DocumentChannel.broadcast_to(document, as_json)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module Collab
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Collab
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Collab
5
+ class Install < Rails::Generators::NamedBase
6
+ include ::Rails::Generators::Migration
7
+
8
+ desc "Creates the necessary migrations and initializer for the gem"
9
+
10
+ source_root File.expand_path('templates', __dir__)
11
+
12
+ def copy_initializer_file
13
+ migration_template(
14
+ "create_collab_tables.rb",
15
+ "db/migrate/create_collab_tables.rb",
16
+ )
17
+
18
+ copy_file "initializer.rb", "config/initializers/collab.rb"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
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: :collaborative_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
@@ -0,0 +1,39 @@
1
+ # Configuration for the collab gem
2
+ Collab.config do |c|
3
+ # The NPM package containing the document schema. This will be require()d from your Rails directory
4
+ # To use a Git repo, see https://docs.npmjs.com/files/package.json#git-urls-as-dependencies
5
+ c.schema_package = "prosemirror-schema-basic"
6
+ # How many old transactions to keep per document
7
+ c.max_transactions = 250
8
+
9
+ # Handlers
10
+ # ========
11
+ # Find a the document to subscribe to based on the params passed to the channel
12
+ # Authorization may also be performed here (raise an error)
13
+ # The block is executed in the scope of the ActionCable channel within #subscribe
14
+ c.find_document_for_subscribe do
15
+ Collab::Models::Document.find params[:document_id]
16
+ end
17
+ # Called when a client submits a transaction in order to update a document
18
+ # You should throw an error if unauthorized
19
+ # The block is executed in the instance of the channel
20
+ c.authorize_update_document do |document, transaction_data|
21
+ # raise "authorization failed"
22
+ end
23
+
24
+ # ActionJob settings
25
+ # ==================
26
+ # The base job class to use
27
+ c.application_job = "::ApplicationJob"
28
+ # The job queue to use for DocumentTransaction jobs
29
+ c.queue_document_transaction_job_as = :default
30
+
31
+
32
+ # ActiveRecord settings
33
+ # =====================
34
+ # The class which models in the gem should inherit from
35
+ c.application_record = "::ApplicationRecord"
36
+ # If you want to use your own document model or document transaction model,
37
+ c.document_model = "::Collab::Models::Document"
38
+ c.document_transaction_model = "::Collab::Models::DocumentTransaction"
39
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :collab do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: collab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Aubin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.3
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.0.3.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.3
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.0.3.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: sqlite3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description:
48
+ email:
49
+ - ben@benaubin.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - Rakefile
55
+ - app/channels/collab/document_channel.rb
56
+ - app/jobs/collab/document_transaction_job.rb
57
+ - lib/collab.rb
58
+ - lib/collab/bridge.rb
59
+ - lib/collab/config.rb
60
+ - lib/collab/has_collaborative_document.rb
61
+ - lib/collab/models/base.rb
62
+ - lib/collab/models/document.rb
63
+ - lib/collab/models/document_transaction.rb
64
+ - lib/collab/railtie.rb
65
+ - lib/collab/version.rb
66
+ - lib/generators/collab/install/install_generator.rb
67
+ - lib/generators/collab/install/templates/create_collab_tables.rb
68
+ - lib/generators/collab/install/templates/initializer.rb
69
+ - lib/tasks/collab_tasks.rake
70
+ homepage: https://github.com/benaubin/rails-collab
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/benaubin/rails-collab
75
+ source_code_uri: https://github.com/benaubin/rails-collab
76
+ changelog_uri: https://github.com/benaubin/rails-collab/blob/master/CHANGELOG.md
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.3.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.1.4
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Collaborative editing on Rails.
96
+ test_files: []