collab 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +27 -0
- data/app/channels/collab/document_channel.rb +28 -0
- data/app/jobs/collab/document_transaction_job.rb +7 -0
- data/lib/collab.rb +19 -0
- data/lib/collab/bridge.rb +54 -0
- data/lib/collab/config.rb +15 -0
- data/lib/collab/has_collaborative_document.rb +19 -0
- data/lib/collab/models/base.rb +6 -0
- data/lib/collab/models/document.rb +75 -0
- data/lib/collab/models/document_transaction.rb +22 -0
- data/lib/collab/railtie.rb +4 -0
- data/lib/collab/version.rb +3 -0
- data/lib/generators/collab/install/install_generator.rb +21 -0
- data/lib/generators/collab/install/templates/create_collab_tables.rb +32 -0
- data/lib/generators/collab/install/templates/initializer.rb +39 -0
- data/lib/tasks/collab_tasks.rake +4 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/lib/collab.rb
ADDED
@@ -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,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,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
|
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: []
|