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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 374c0f738381bbb9e6b5537e995fedb58f3d6134e2a127d35ee1f9cf5dacc213
4
- data.tar.gz: 1219f6ff5ebabfeecbd412a66f54d48744964ab67998137f3d5f1ea746c04e88
3
+ metadata.gz: be283fc00ae2733bc0459e7c394e48b96359614de335b4d15cefec53a2134763
4
+ data.tar.gz: 7cc650231d0bc1537af64d27ebd748422b722a94be71108809d18024640472a8
5
5
  SHA512:
6
- metadata.gz: '092dcf7715ef30e5d309c7e6332218098ac190985a4e58b4d26384c26803815bc2029066b4e93afcdf4039f24bdfc51fd8891ccde9add989296e5956d9786408'
7
- data.tar.gz: b2bfaa08e8673940757d8673c66b7d2dba11cf5b857c8ea1e66b5e4f2de955e4bcead0ebb6de9db84a9fcb443c9d2e6ed7391fd5cb4a8dbd54945f60fee3aba4
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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboLive
4
+ class ComponentsController < ActionController::API
5
+ def update
6
+ stream = Renderer.render params.to_unsafe_hash
7
+ render plain: stream
8
+ end
9
+ end
10
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ TurboLive::Engine.routes.draw do
2
+ post "" => "components#update"
3
+ end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboLive
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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
- # Your code goes here...
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,7 @@
1
+ // Import controllers here
2
+ import registerTurboLiveChannel from "./turbo_live_channel.js"
3
+
4
+ export default function (consumer) {
5
+ // Register channels here
6
+ registerTurboLiveChannel(consumer)
7
+ }
@@ -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,7 @@
1
+ // Import controllers here
2
+ import TurboLiveController from "./turbo_live_controller.js"
3
+
4
+ export default function (application) {
5
+ // Register controllers here
6
+ application.register("turbo-live", TurboLiveController)
7
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ import registerControllers from "./controllers/register_controllers.js"
2
+ import registerChannels from "./channels/register_channels.js"
3
+
4
+
5
+ export { registerControllers, registerChannels }
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.0
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-11 00:00:00.000000000 Z
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