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 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