turbo_live 0.1.0 → 0.1.1
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 +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
|