action_merge 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/LICENSE.md +157 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/app/assets/builds/action_merge/application.d.ts +1 -0
- data/app/assets/builds/action_merge/application.d.ts.map +1 -0
- data/app/assets/builds/action_merge/application.js +6 -0
- data/app/assets/builds/action_merge/application.js.map +1 -0
- data/app/assets/builds/action_merge/automerge.wasm +0 -0
- data/app/assets/builds/action_merge/network-adapter.d.ts +18 -0
- data/app/assets/builds/action_merge/network-adapter.d.ts.map +1 -0
- data/app/assets/builds/action_merge/syncable.d.ts +7 -0
- data/app/assets/builds/action_merge/syncable.d.ts.map +1 -0
- data/app/assets/config/action_merge_manifest.js +1 -0
- data/app/assets/stylesheets/action_merge/application.css +15 -0
- data/app/channels/action_merge/sync_channel.rb +36 -0
- data/app/channels/application_cable/channel.rb +4 -0
- data/app/channels/application_cable/connection.rb +4 -0
- data/app/controllers/action_merge/application_controller.rb +4 -0
- data/app/helpers/action_merge/application_helper.rb +4 -0
- data/app/javascript/action_merge/application.ts +5 -0
- data/app/javascript/action_merge/network-adapter.ts +144 -0
- data/app/javascript/action_merge/syncable.ts +24 -0
- data/app/jobs/action_merge/application_job.rb +4 -0
- data/app/mailers/action_merge/application_mailer.rb +6 -0
- data/app/models/action_merge/application_record.rb +5 -0
- data/app/models/action_merge/document/chunk.rb +11 -0
- data/app/models/action_merge/document.rb +6 -0
- data/app/models/action_merge/peer.rb +23 -0
- data/app/models/action_merge/storage_adapter.rb +32 -0
- data/app/views/layouts/action_merge/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20240910160133_create_action_merge_documents.rb +19 -0
- data/lib/action_merge/assets.rb +19 -0
- data/lib/action_merge/engine.rb +16 -0
- data/lib/action_merge/helpers.rb +7 -0
- data/lib/action_merge/syncable.rb +19 -0
- data/lib/action_merge.rb +12 -0
- data/lib/tasks/action_merge_tasks.rake +4 -0
- metadata +104 -0
Binary file
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { Consumer, Subscription } from '@rails/actioncable';
|
2
|
+
import { NetworkAdapter, PeerId, PeerMetadata, Message } from '@automerge/automerge-repo/slim';
|
3
|
+
declare class ActionCableNetworkAdapter extends NetworkAdapter {
|
4
|
+
#private;
|
5
|
+
channelName: string;
|
6
|
+
consumer: Consumer;
|
7
|
+
subscription?: Subscription;
|
8
|
+
syncableType: string;
|
9
|
+
syncableId: any;
|
10
|
+
constructor(channelName: string, syncableType: string, syncableId: any, serverUrl?: string);
|
11
|
+
connect(peerId: PeerId, peerMetadata?: PeerMetadata): void;
|
12
|
+
disconnect(): void;
|
13
|
+
send(message: Message): void;
|
14
|
+
whenReady(): Promise<void>;
|
15
|
+
isReady(): boolean;
|
16
|
+
}
|
17
|
+
export default ActionCableNetworkAdapter;
|
18
|
+
//# sourceMappingURL=network-adapter.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"network-adapter.d.ts","sourceRoot":"","sources":["../../../javascript/action_merge/network-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,QAAQ,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAQ,MAAM,gCAAgC,CAAA;AAGpG,cAAM,yBAA0B,SAAQ,cAAc;;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAElB,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,GAAG,CAAA;gBACH,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM;IAS1F,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY;IAyEnD,UAAU;IAeV,IAAI,CAAC,OAAO,EAAE,OAAO;IAmBrB,SAAS;IAWT,OAAO;CAGP;AAED,eAAe,yBAAyB,CAAA"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"syncable.d.ts","sourceRoot":"","sources":["../../../javascript/action_merge/syncable.ts"],"names":[],"mappings":"AAIA,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,MAAM;QAAG,QAAQ,CAAC,EAAE,GAAG,CAAA;KAAE;CACnC"}
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/action_merge .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
|
3
|
+
module ActionMerge
|
4
|
+
class SyncChannel < ActionCable::Channel::Base
|
5
|
+
cattr_accessor :peers
|
6
|
+
@@peers ||= {}
|
7
|
+
|
8
|
+
def subscribed
|
9
|
+
@model = ActiveSupport::Inflector.constantize(params[:syncable_type]).find(params[:syncable_id])
|
10
|
+
|
11
|
+
reject unless @model.authorize_sync(self)
|
12
|
+
|
13
|
+
@peer = ActionMerge::Peer.new(params[:id], storageId: params.dig(:metadata, :storageId), isEphemeral: params.dig(:metadata, :isEphemeral), model: @model)
|
14
|
+
|
15
|
+
@@peers[@peer.to_gid_param] = @peer
|
16
|
+
|
17
|
+
stream_for @peer
|
18
|
+
end
|
19
|
+
|
20
|
+
def perform_action(data)
|
21
|
+
data.deep_symbolize_keys!
|
22
|
+
if data.dig(:targetId).presence
|
23
|
+
self.class.broadcast_to(@@peers[data[:targetId]], data)
|
24
|
+
else
|
25
|
+
@@peers.each do |id, peer|
|
26
|
+
self.class.broadcast_to(peer, data) unless id == @peer&.to_gid_param
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def unsubscribed
|
32
|
+
@@peers[@peer&.to_gid_param] = nil
|
33
|
+
@peer = nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import { createConsumer, Consumer, Subscription } from "@rails/actioncable"
|
2
|
+
import { NetworkAdapter, PeerId, PeerMetadata, Message, cbor } from "@automerge/automerge-repo/slim"
|
3
|
+
import omit from "lodash.omit"
|
4
|
+
|
5
|
+
class ActionCableNetworkAdapter extends NetworkAdapter {
|
6
|
+
channelName: string
|
7
|
+
consumer: Consumer
|
8
|
+
#isReady: boolean
|
9
|
+
subscription?: Subscription
|
10
|
+
syncableType: string
|
11
|
+
syncableId: any
|
12
|
+
constructor(channelName: string, syncableType: string, syncableId: any, serverUrl?: string) {
|
13
|
+
super()
|
14
|
+
this.channelName = channelName
|
15
|
+
this.syncableType = syncableType
|
16
|
+
this.syncableId = syncableId
|
17
|
+
this.consumer = createConsumer(serverUrl)
|
18
|
+
this.#isReady = false
|
19
|
+
}
|
20
|
+
|
21
|
+
connect(peerId: PeerId, peerMetadata?: PeerMetadata) {
|
22
|
+
this.peerId = peerId
|
23
|
+
this.peerMetadata = peerMetadata
|
24
|
+
|
25
|
+
this.subscription = this.consumer.subscriptions.create({ channel: this.channelName, id: this.peerId, metadata: this.peerMetadata || {}, syncable_id: this.syncableId, syncable_type: this.syncableType }, {
|
26
|
+
connected: () => {
|
27
|
+
this.#isReady = true
|
28
|
+
this.send({
|
29
|
+
// @ts-expect-error
|
30
|
+
senderId: this.peerId,
|
31
|
+
type: "arrive",
|
32
|
+
// @ts-expect-error
|
33
|
+
targetId: null
|
34
|
+
})
|
35
|
+
},
|
36
|
+
|
37
|
+
disconnected: () => {
|
38
|
+
this.#isReady = false
|
39
|
+
this.emit("close")
|
40
|
+
},
|
41
|
+
|
42
|
+
received: (message) => {
|
43
|
+
// @ts-ignore
|
44
|
+
window.message = message
|
45
|
+
if (message.targetId != null && message.targetId !== this.peerId) {
|
46
|
+
if (message.senderId == this.peerId) {
|
47
|
+
return
|
48
|
+
}
|
49
|
+
throw new Error(
|
50
|
+
"ActionCableNetworkAdapter should never receive messages for a different peer."
|
51
|
+
)
|
52
|
+
}
|
53
|
+
|
54
|
+
const { senderId, type } = message
|
55
|
+
|
56
|
+
switch (type) {
|
57
|
+
case "arrive":
|
58
|
+
{
|
59
|
+
const { peerMetadata } = message
|
60
|
+
this.send({
|
61
|
+
type: "welcome",
|
62
|
+
// @ts-expect-error
|
63
|
+
senderId: this.peerId,
|
64
|
+
peerMetadata: this.peerMetadata,
|
65
|
+
targetId: senderId,
|
66
|
+
})
|
67
|
+
this.emit("peer-candidate", { peerId: senderId, peerMetadata })
|
68
|
+
}
|
69
|
+
break
|
70
|
+
case "welcome":
|
71
|
+
{
|
72
|
+
const { peerMetadata } = message
|
73
|
+
this.emit("peer-candidate", { peerId: senderId, peerMetadata })
|
74
|
+
}
|
75
|
+
break
|
76
|
+
case "leave":
|
77
|
+
this.emit("peer-disconnected", { peerId: senderId })
|
78
|
+
default:
|
79
|
+
if(message.data && Object.values(message.data).length > 0) {
|
80
|
+
// @ts-ignore
|
81
|
+
this.emit("message", {
|
82
|
+
...omit(message, "data"),
|
83
|
+
data: new Uint8Array(Object.values(message.data)),
|
84
|
+
})
|
85
|
+
} else {
|
86
|
+
this.emit("message", message)
|
87
|
+
}
|
88
|
+
break
|
89
|
+
}
|
90
|
+
},
|
91
|
+
})
|
92
|
+
}
|
93
|
+
|
94
|
+
disconnect() {
|
95
|
+
if (this.subscription) {
|
96
|
+
this.send({
|
97
|
+
type: "leave",
|
98
|
+
// @ts-expect-error
|
99
|
+
senderId: this.peerId,
|
100
|
+
})
|
101
|
+
// @ts-expect-error
|
102
|
+
this.emit("peer-disconnected", { peerId: this.peerId })
|
103
|
+
this.subscription.unsubscribe()
|
104
|
+
this.subscription = undefined
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
|
109
|
+
send(message: Message) {
|
110
|
+
this.subscription?.send(message)
|
111
|
+
if (message.data) {
|
112
|
+
const data = message.data.buffer.slice(
|
113
|
+
message.data.byteOffset,
|
114
|
+
message.data.byteOffset + message.data.byteLength
|
115
|
+
)
|
116
|
+
|
117
|
+
this.subscription?.send(
|
118
|
+
{
|
119
|
+
...message,
|
120
|
+
data,
|
121
|
+
}
|
122
|
+
)
|
123
|
+
} else {
|
124
|
+
this.subscription?.send(message)
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
whenReady() {
|
129
|
+
return new Promise<void>((resolve) => {
|
130
|
+
while (true) {
|
131
|
+
if (this.#isReady) {
|
132
|
+
break
|
133
|
+
}
|
134
|
+
}
|
135
|
+
resolve()
|
136
|
+
})
|
137
|
+
}
|
138
|
+
|
139
|
+
isReady() {
|
140
|
+
return this.#isReady
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
export default ActionCableNetworkAdapter
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { Repo, type RepoConfig } from "@automerge/automerge-repo";
|
2
|
+
import ActionCableNetworkAdapter from "./network-adapter.js"
|
3
|
+
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb"
|
4
|
+
|
5
|
+
declare global {
|
6
|
+
interface Window { Syncable?: any }
|
7
|
+
}
|
8
|
+
|
9
|
+
type Syncable = {
|
10
|
+
new (klass: string, id: any, channel?: string, obj?: RepoConfig): Repo
|
11
|
+
}
|
12
|
+
|
13
|
+
const Syncable: Syncable = new Proxy(Repo, {
|
14
|
+
construct: (target, args) => {
|
15
|
+
const [ klass, id, channel = "ActionMerge::SyncChannel", obj = {} ] = args
|
16
|
+
return new target({
|
17
|
+
network: [new ActionCableNetworkAdapter(channel, klass, id)],
|
18
|
+
storage: new IndexedDBStorageAdapter(),
|
19
|
+
...obj
|
20
|
+
});
|
21
|
+
}
|
22
|
+
}) as Syncable
|
23
|
+
|
24
|
+
window.Syncable = Syncable
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActionMerge
|
2
|
+
class Peer
|
3
|
+
attr_accessor :id, :isEphemeral, :storageId, :model
|
4
|
+
|
5
|
+
def initialize(id, isEphemeral: nil, storageId: nil, model: nil)
|
6
|
+
@id = id
|
7
|
+
@isEphemeral = isEphemeral
|
8
|
+
@storageId = storageId
|
9
|
+
@model = model
|
10
|
+
end
|
11
|
+
|
12
|
+
def metadata
|
13
|
+
{
|
14
|
+
isEphemeral:,
|
15
|
+
storageId:
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_gid_param
|
20
|
+
[[@model.class.name, @model.id], id]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# @implements automerge.org/automerge-repo/interfaces/_automerge_automerge_repo.StorageAdapterInterface
|
2
|
+
module ActionMerge
|
3
|
+
module StorageAdapter
|
4
|
+
def load(key)
|
5
|
+
ActionMerge::Document::Chunk.find(key)
|
6
|
+
end
|
7
|
+
|
8
|
+
def loadRange(key)
|
9
|
+
document_id, type = *key
|
10
|
+
|
11
|
+
d = ActionMerge::Document.find(document_id)
|
12
|
+
|
13
|
+
if type
|
14
|
+
d.chunks.where(type:)
|
15
|
+
else
|
16
|
+
d.chunks
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove(key)
|
21
|
+
ActionMerge::Document::Chunk.destroy(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def removeRange(key)
|
25
|
+
loadRange(key).destroy_all
|
26
|
+
end
|
27
|
+
|
28
|
+
def save(key, value)
|
29
|
+
ActionMerge::Document::Chunk.upsert(key, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Action Merge</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "action_merge/application", media: "all" %>
|
9
|
+
<%= javascript_include_tag "action_merge/application", "data-turbo-track": "reload", type: "module" %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<%= yield %>
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateActionMergeDocuments < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :action_merge_documents, primary_key: :id, id: false do |t|
|
4
|
+
t.string :id
|
5
|
+
t.string :syncable_type
|
6
|
+
t.integer :syncable_id
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table :action_merge_document_chunks, primary_key: [:action_merge_document_id, :type, :hash], id: false do |t|
|
12
|
+
t.string :type
|
13
|
+
t.text :hash
|
14
|
+
t.belongs_to :action_merge_document
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActionMerge
|
2
|
+
class Assets
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
path = env[Rack::PATH_INFO]
|
9
|
+
|
10
|
+
if path.start_with? "/.well-known/action_merge/assets/"
|
11
|
+
env[Rack::PATH_INFO] = path.split("/").last
|
12
|
+
|
13
|
+
Rack::Files.new(ActionMerge::Engine.root.join("app", "assets", "builds", "action_merge")).call(env)
|
14
|
+
else
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActionMerge
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace ActionMerge
|
4
|
+
|
5
|
+
initializer "action_merge.assets" do |app|
|
6
|
+
app.middleware.insert_after(
|
7
|
+
Rack::Sendfile,
|
8
|
+
ActionMerge::Assets
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "action_merge.helpers" do
|
13
|
+
ActiveSupport.on_load(:action_view) { include ActionMerge::Helpers }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActionMerge
|
2
|
+
module Syncable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_one :automerge_document, dependent: :destroy, as: :syncable
|
7
|
+
end
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def to_authorize_sync(&block)
|
11
|
+
@@_to_authorize_sync = block
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def authorize_sync(channel)
|
16
|
+
channel.instance_eval &@@_to_authorize_sync
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/action_merge.rb
ADDED
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: action_merge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Reese Armstrong
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-16 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: '7.1'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '7.1'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8.0'
|
33
|
+
description: Build local-first and collaborative Rails applications with Automerge
|
34
|
+
email:
|
35
|
+
- me@reeseric.ci
|
36
|
+
executables: []
|
37
|
+
extensions: []
|
38
|
+
extra_rdoc_files: []
|
39
|
+
files:
|
40
|
+
- LICENSE.md
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- app/assets/builds/action_merge/application.d.ts
|
44
|
+
- app/assets/builds/action_merge/application.d.ts.map
|
45
|
+
- app/assets/builds/action_merge/application.js
|
46
|
+
- app/assets/builds/action_merge/application.js.map
|
47
|
+
- app/assets/builds/action_merge/automerge.wasm
|
48
|
+
- app/assets/builds/action_merge/network-adapter.d.ts
|
49
|
+
- app/assets/builds/action_merge/network-adapter.d.ts.map
|
50
|
+
- app/assets/builds/action_merge/syncable.d.ts
|
51
|
+
- app/assets/builds/action_merge/syncable.d.ts.map
|
52
|
+
- app/assets/config/action_merge_manifest.js
|
53
|
+
- app/assets/stylesheets/action_merge/application.css
|
54
|
+
- app/channels/action_merge/sync_channel.rb
|
55
|
+
- app/channels/application_cable/channel.rb
|
56
|
+
- app/channels/application_cable/connection.rb
|
57
|
+
- app/controllers/action_merge/application_controller.rb
|
58
|
+
- app/helpers/action_merge/application_helper.rb
|
59
|
+
- app/javascript/action_merge/application.ts
|
60
|
+
- app/javascript/action_merge/network-adapter.ts
|
61
|
+
- app/javascript/action_merge/syncable.ts
|
62
|
+
- app/jobs/action_merge/application_job.rb
|
63
|
+
- app/mailers/action_merge/application_mailer.rb
|
64
|
+
- app/models/action_merge/application_record.rb
|
65
|
+
- app/models/action_merge/document.rb
|
66
|
+
- app/models/action_merge/document/chunk.rb
|
67
|
+
- app/models/action_merge/peer.rb
|
68
|
+
- app/models/action_merge/storage_adapter.rb
|
69
|
+
- app/views/layouts/action_merge/application.html.erb
|
70
|
+
- config/routes.rb
|
71
|
+
- db/migrate/20240910160133_create_action_merge_documents.rb
|
72
|
+
- lib/action_merge.rb
|
73
|
+
- lib/action_merge/assets.rb
|
74
|
+
- lib/action_merge/engine.rb
|
75
|
+
- lib/action_merge/helpers.rb
|
76
|
+
- lib/action_merge/syncable.rb
|
77
|
+
- lib/tasks/action_merge_tasks.rake
|
78
|
+
homepage: https://codeberg.org/reesericci/action_merge
|
79
|
+
licenses:
|
80
|
+
- LGPL-3.0-or-later
|
81
|
+
metadata:
|
82
|
+
homepage_uri: https://codeberg.org/reesericci/action_merge
|
83
|
+
source_code_uri: https://codeberg.org/reesericci/action_merge
|
84
|
+
changelog_uri: https://codeberg.org/reesericci/action_merge/releases
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubygems_version: 3.5.9
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: Build local-first and collaborative Rails applications
|
104
|
+
test_files: []
|