collab 0.1.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 +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: []
|