turbo_live 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/channels/components_channel.rb +20 -0
- data/app/controllers/turbo_live/components_controller.rb +10 -0
- data/config/routes.rb +3 -0
- data/lib/turbo_live/component.rb +85 -0
- data/lib/turbo_live/engine.rb +13 -0
- data/lib/turbo_live/renderer.rb +55 -0
- data/lib/turbo_live/version.rb +1 -1
- data/lib/turbo_live.rb +17 -1
- data/package-lock.json +33 -0
- data/package.json +29 -0
- data/src/.npmignore +1 -0
- data/src/js/channels/register_channels.js +7 -0
- data/src/js/channels/turbo_live_channel.js +31 -0
- data/src/js/controllers/register_controllers.js +7 -0
- data/src/js/controllers/turbo_live_controller.js +116 -0
- data/src/js/core.js +5 -0
- metadata +45 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be283fc00ae2733bc0459e7c394e48b96359614de335b4d15cefec53a2134763
|
4
|
+
data.tar.gz: 7cc650231d0bc1537af64d27ebd748422b722a94be71108809d18024640472a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de85e08541c5fb65b2ecace8785f3688fc3b121f01a4a73991e7f25a21f935f7f4584913ca5c9a54ce99700da247ec1850d5898722fb44bd926a878d4eb84e33
|
7
|
+
data.tar.gz: e0d1b93943354bbde2cb5e0654966bb0be5f27bcc68d54b2772440e67ff70697742554b9dd116146c4146dcb52b657f092856dd4512fd0d2bd6a1fe5f2f1e644
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboLive
|
4
|
+
class ComponentsChannel < ActionCable::Channel::Base
|
5
|
+
def subscribed
|
6
|
+
stream_from stream_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def receive(params)
|
10
|
+
stream = Renderer.render params
|
11
|
+
ActionCable.server.broadcast(stream_name, stream)
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def stream_name
|
17
|
+
@stream_name ||= "turbo_live-#{SecureRandom.hex}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "phlex"
|
4
|
+
|
5
|
+
module TurboLive
|
6
|
+
class Component < Phlex::HTML
|
7
|
+
extend Literal::Properties
|
8
|
+
|
9
|
+
SUPPORTED_EVENTS = %i[click change].freeze
|
10
|
+
|
11
|
+
def self.state(name, type, **options, &block)
|
12
|
+
options = {reader: :public, writer: :protected}.merge(**options).compact
|
13
|
+
prop(name, _Nilable(type), **options, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
state :live_id, String, writer: nil do |value|
|
17
|
+
value || SecureRandom.hex
|
18
|
+
end
|
19
|
+
|
20
|
+
def view_template
|
21
|
+
div(
|
22
|
+
id: verifiable_live_id,
|
23
|
+
style: "display: contents;",
|
24
|
+
data_controller: "turbo-live",
|
25
|
+
data_turbo_live_component_value: to_verifiable(serialize)
|
26
|
+
) do
|
27
|
+
view
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def update(input)
|
32
|
+
end
|
33
|
+
|
34
|
+
def verifiable_live_id
|
35
|
+
to_verifiable(live_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def on(**mappings)
|
41
|
+
actions = []
|
42
|
+
params = []
|
43
|
+
mappings.each do |event, param|
|
44
|
+
raise NotImplementedError, "TurboLive does not support '#{event}' events" unless SUPPORTED_EVENTS.include?(event)
|
45
|
+
|
46
|
+
actions << "#{event}->turbo-live#on#{event.capitalize}"
|
47
|
+
params << [:"data_turbo_live_#{event}_param", to_verifiable(param)]
|
48
|
+
end
|
49
|
+
|
50
|
+
data_action = actions.join(" ")
|
51
|
+
params.to_h.merge(data_action: data_action)
|
52
|
+
end
|
53
|
+
|
54
|
+
def every(milliseconds, event)
|
55
|
+
data = {milliseconds => to_verifiable(event)}.to_json
|
56
|
+
add_data :every, data
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def add_data(type, value)
|
62
|
+
# Temporary hack to embed data.
|
63
|
+
# Switch to HTML templates
|
64
|
+
div(
|
65
|
+
class: "turbo-live-data",
|
66
|
+
data_turbo_live_id: verifiable_live_id,
|
67
|
+
data_turbo_live_data_type: type,
|
68
|
+
data_turbo_live_data_value: value,
|
69
|
+
style: "display: none;", display: :none
|
70
|
+
) {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def serialize
|
74
|
+
state = self.class.literal_properties.map do |prop|
|
75
|
+
[prop.name, instance_variable_get(:"@#{prop.name}")]
|
76
|
+
end.to_h
|
77
|
+
|
78
|
+
{klass: self.class.to_s, state: state}
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_verifiable(value)
|
82
|
+
TurboLive.verifier.generate value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboLive
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace TurboLive
|
6
|
+
|
7
|
+
initializer "turbo_live.verifier_key" do
|
8
|
+
config.after_initialize do
|
9
|
+
TurboLive.verifier_key = Rails.application.key_generator.generate_key("turbo_live/verifier_key")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TurboLive
|
4
|
+
class Renderer
|
5
|
+
class << self
|
6
|
+
def render(data)
|
7
|
+
data = data.symbolize_keys
|
8
|
+
# build the payload
|
9
|
+
payload = extract_payload(data)
|
10
|
+
# create the component
|
11
|
+
component = build_component(data)
|
12
|
+
# run the update function
|
13
|
+
component.update payload
|
14
|
+
# render the replace stream
|
15
|
+
<<~STREAM
|
16
|
+
<turbo-stream action="replace" target="#{data[:id]}">
|
17
|
+
<template>
|
18
|
+
#{component.call}
|
19
|
+
</template>
|
20
|
+
</turbo-stream>
|
21
|
+
STREAM
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def extract_payload(data)
|
27
|
+
payload_event = from_verifiable(data[:payload][0])
|
28
|
+
if data[:payload].size == 2
|
29
|
+
[payload_event, data[:payload][1]]
|
30
|
+
else
|
31
|
+
[payload_event]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_component(data)
|
36
|
+
component_data = from_verifiable(data[:component])
|
37
|
+
component_klass = component_data[:klass].safe_constantize
|
38
|
+
# Ensure we have a correct class
|
39
|
+
unless component_klass.is_a?(Class) && component_klass < Component
|
40
|
+
raise ArgumentError, "[IMPORTANT!!!] Unexpected class: #{component_klass}"
|
41
|
+
end
|
42
|
+
|
43
|
+
component = component_klass.new(**component_data[:state])
|
44
|
+
# rudimentary checksum to ensure id matches
|
45
|
+
raise ArgumentError, "component ID mismatch" unless component.verifiable_live_id == data[:id]
|
46
|
+
|
47
|
+
component
|
48
|
+
end
|
49
|
+
|
50
|
+
def from_verifiable(verifiable)
|
51
|
+
TurboLive.verifier.verified verifiable
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/turbo_live/version.rb
CHANGED
data/lib/turbo_live.rb
CHANGED
@@ -1,8 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "turbo_live/version"
|
4
|
+
require_relative "turbo_live/component"
|
5
|
+
require_relative "turbo_live/renderer"
|
6
|
+
|
7
|
+
require_relative "turbo_live/engine" if defined?(Rails)
|
8
|
+
require_relative "../app/channels/components_channel" if defined?(ActionCable)
|
4
9
|
|
5
10
|
module TurboLive
|
6
11
|
class Error < StandardError; end
|
7
|
-
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_writer :verifier_key
|
15
|
+
|
16
|
+
def verifier
|
17
|
+
@verifier ||= ActiveSupport::MessageVerifier.new(verifier_key, digest: "SHA256", serializer: YAML)
|
18
|
+
end
|
19
|
+
|
20
|
+
def verifier_key
|
21
|
+
@verifier_key or raise ArgumentError, "Turbo requires a verifier_key"
|
22
|
+
end
|
23
|
+
end
|
8
24
|
end
|
data/package-lock.json
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"name": "@radioactive-labs/turbo-live",
|
3
|
+
"version": "0.1.1",
|
4
|
+
"lockfileVersion": 3,
|
5
|
+
"requires": true,
|
6
|
+
"packages": {
|
7
|
+
"": {
|
8
|
+
"name": "@radioactive-labs/turbo-live",
|
9
|
+
"version": "0.1.1",
|
10
|
+
"license": "MIT",
|
11
|
+
"dependencies": {
|
12
|
+
"@hotwired/stimulus": "^3.2.2",
|
13
|
+
"@hotwired/turbo": "^8.0.4"
|
14
|
+
},
|
15
|
+
"devDependencies": {}
|
16
|
+
},
|
17
|
+
"node_modules/@hotwired/stimulus": {
|
18
|
+
"version": "3.2.2",
|
19
|
+
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
|
20
|
+
"integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==",
|
21
|
+
"license": "MIT"
|
22
|
+
},
|
23
|
+
"node_modules/@hotwired/turbo": {
|
24
|
+
"version": "8.0.10",
|
25
|
+
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.10.tgz",
|
26
|
+
"integrity": "sha512-xen1YhNQirAHlA8vr/444XsTNITC1Il2l/Vx4w8hAWPpI5nQO78mVHNsmFuayETodzPwh25ob2TgfCEV/Loiog==",
|
27
|
+
"license": "MIT",
|
28
|
+
"engines": {
|
29
|
+
"node": ">= 14"
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
data/package.json
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"name": "@radioactive-labs/turbo-live",
|
3
|
+
"version": "0.1.1",
|
4
|
+
"description": "Stateless live components for Ruby applications",
|
5
|
+
"type": "module",
|
6
|
+
"main": "src/js/core.js",
|
7
|
+
"files": [
|
8
|
+
"src/"
|
9
|
+
],
|
10
|
+
"author": "Stefan Froelich (@thedumbtechguy)",
|
11
|
+
"license": "MIT",
|
12
|
+
"bugs": {
|
13
|
+
"url": "https://github.com/radioactive-labs/turbo_live/issues"
|
14
|
+
},
|
15
|
+
"repository": {
|
16
|
+
"type": "git",
|
17
|
+
"url": "git+https://github.com/radioactive-labs/turbo_live.git"
|
18
|
+
},
|
19
|
+
"publishConfig": {
|
20
|
+
"access": "public"
|
21
|
+
},
|
22
|
+
"homepage": "https://github.com/radioactive-labs/turbo_live#readme",
|
23
|
+
"dependencies": {
|
24
|
+
"@hotwired/stimulus": "^3.2.2",
|
25
|
+
"@hotwired/turbo": "^8.0.4"
|
26
|
+
},
|
27
|
+
"devDependencies": {},
|
28
|
+
"scripts": {}
|
29
|
+
}
|
data/src/.npmignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
build/
|
@@ -0,0 +1,31 @@
|
|
1
|
+
export default function (consumer) {
|
2
|
+
consumer.subscriptions.create({ channel: "TurboLive::ComponentsChannel" }, {
|
3
|
+
// Called once when the subscription is created.
|
4
|
+
initialized() {
|
5
|
+
console.log("TurboLiveChannel initialized")
|
6
|
+
},
|
7
|
+
|
8
|
+
// Called when the subscription is ready for use on the server.
|
9
|
+
connected() {
|
10
|
+
console.log("TurboLiveChannel connected")
|
11
|
+
window.turboLive = this;
|
12
|
+
},
|
13
|
+
|
14
|
+
received(turbo_stream) {
|
15
|
+
console.log("TurboLiveChannel received", turbo_stream)
|
16
|
+
Turbo.renderStreamMessage(turbo_stream);
|
17
|
+
},
|
18
|
+
|
19
|
+
// Called when the WebSocket connection is closed.
|
20
|
+
disconnected() {
|
21
|
+
console.log("TurboLiveChannel disconnected")
|
22
|
+
window.turboLive = null;
|
23
|
+
},
|
24
|
+
|
25
|
+
// Called when the subscription is rejected by the server.
|
26
|
+
rejected() {
|
27
|
+
console.log("TurboLiveChannel rejected")
|
28
|
+
window.turboLive = null;
|
29
|
+
},
|
30
|
+
})
|
31
|
+
}
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static values = {
|
5
|
+
id: String,
|
6
|
+
component: String,
|
7
|
+
}
|
8
|
+
|
9
|
+
get component() {
|
10
|
+
return this.componentValue
|
11
|
+
}
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
console.log("TurboLiveController connected:", this.element.id, this.component)
|
15
|
+
|
16
|
+
this.intervals = []
|
17
|
+
|
18
|
+
this.#readEmbeddedData()
|
19
|
+
}
|
20
|
+
|
21
|
+
disconnect() {
|
22
|
+
console.log("TurboLiveController disconnected")
|
23
|
+
this.#clearIntervals()
|
24
|
+
}
|
25
|
+
|
26
|
+
dispatch(event, payload) {
|
27
|
+
console.log("TurboLiveController dispatch:", this.element.id, event, payload)
|
28
|
+
let data = { id: this.element.id, event: event, payload: payload, component: this.component }
|
29
|
+
if (window.turboLive) {
|
30
|
+
console.log("TurboLiveController dispatching via websockets")
|
31
|
+
window.turboLive.send(data)
|
32
|
+
}
|
33
|
+
else {
|
34
|
+
console.log("TurboLiveController dispatching via HTTP")
|
35
|
+
|
36
|
+
fetch('/turbo_live', {
|
37
|
+
method: 'POST',
|
38
|
+
headers: {
|
39
|
+
'Content-Type': 'application/json',
|
40
|
+
},
|
41
|
+
body: JSON.stringify(data)
|
42
|
+
})
|
43
|
+
.then(response => {
|
44
|
+
if (!response.ok) {
|
45
|
+
throw new Error(`Network response was not OK`);
|
46
|
+
}
|
47
|
+
return response.text();
|
48
|
+
})
|
49
|
+
.then(turbo_stream => {
|
50
|
+
console.log('TurboLiveController dispatch success:', turbo_stream);
|
51
|
+
Turbo.renderStreamMessage(turbo_stream);
|
52
|
+
})
|
53
|
+
.catch((error) => {
|
54
|
+
console.error('TurboLiveController dispatch error:', error);
|
55
|
+
});
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
onClick(event) {
|
60
|
+
// event.preventDefault();
|
61
|
+
console.log("TurboLiveController onClick")
|
62
|
+
this.#dispatchSimpleEvent("click", event)
|
63
|
+
}
|
64
|
+
|
65
|
+
onChange(event) {
|
66
|
+
// event.preventDefault();
|
67
|
+
console.log("TurboLiveController onChange")
|
68
|
+
this.#dispatchValueEvent("change", event)
|
69
|
+
}
|
70
|
+
|
71
|
+
#readEmbeddedData() {
|
72
|
+
this.element.querySelectorAll(".turbo-live-data").forEach((element) => {
|
73
|
+
if (this.element.id != element.dataset.turboLiveId) return;
|
74
|
+
|
75
|
+
let type = element.dataset.turboLiveDataType
|
76
|
+
let value = JSON.parse(element.dataset.turboLiveDataValue)
|
77
|
+
switch (type) {
|
78
|
+
case "every":
|
79
|
+
this.#setupInterval(value)
|
80
|
+
break;
|
81
|
+
}
|
82
|
+
})
|
83
|
+
}
|
84
|
+
|
85
|
+
#setupInterval(intervalConfig) {
|
86
|
+
try {
|
87
|
+
for (let interval in intervalConfig) {
|
88
|
+
this.intervals.push(
|
89
|
+
setInterval(() => {
|
90
|
+
this.dispatch("every", [intervalConfig[interval]])
|
91
|
+
}, interval)
|
92
|
+
)
|
93
|
+
}
|
94
|
+
}
|
95
|
+
catch (e) {
|
96
|
+
console.error(e)
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
#clearIntervals() {
|
101
|
+
this.intervals.forEach((interval) => {
|
102
|
+
clearInterval(interval)
|
103
|
+
})
|
104
|
+
}
|
105
|
+
|
106
|
+
#dispatchSimpleEvent(name, { params }) {
|
107
|
+
let live_event = params[name]
|
108
|
+
this.dispatch(name, [live_event])
|
109
|
+
}
|
110
|
+
|
111
|
+
#dispatchValueEvent(name, { params, target }) {
|
112
|
+
let value = target.value
|
113
|
+
let live_event = params[name]
|
114
|
+
this.dispatch(name, [live_event, value])
|
115
|
+
}
|
116
|
+
}
|
data/src/js/core.js
ADDED
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo_live
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TheDumbTechGuy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-10-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: phlex-rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: literal
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
description: Stateless live components for Ruby applications that work over Websockets
|
14
42
|
and HTTP.
|
15
43
|
email:
|
@@ -24,9 +52,23 @@ files:
|
|
24
52
|
- LICENSE.txt
|
25
53
|
- README.md
|
26
54
|
- Rakefile
|
55
|
+
- app/channels/components_channel.rb
|
56
|
+
- app/controllers/turbo_live/components_controller.rb
|
57
|
+
- config/routes.rb
|
27
58
|
- lib/turbo_live.rb
|
59
|
+
- lib/turbo_live/component.rb
|
60
|
+
- lib/turbo_live/engine.rb
|
61
|
+
- lib/turbo_live/renderer.rb
|
28
62
|
- lib/turbo_live/version.rb
|
63
|
+
- package-lock.json
|
64
|
+
- package.json
|
29
65
|
- sig/turbo_live.rbs
|
66
|
+
- src/.npmignore
|
67
|
+
- src/js/channels/register_channels.js
|
68
|
+
- src/js/channels/turbo_live_channel.js
|
69
|
+
- src/js/controllers/register_controllers.js
|
70
|
+
- src/js/controllers/turbo_live_controller.js
|
71
|
+
- src/js/core.js
|
30
72
|
homepage: https://github.com/radioactive-labs/turbo_live
|
31
73
|
licenses:
|
32
74
|
- MIT
|