Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor SR client package and introduce the Reflex class #592

Merged
merged 28 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
293a3ce
created Stimulus.app
leastbad Jun 25, 2022
75735a7
elementToXPath supports html element
leastbad Jun 29, 2022
2790a95
reflexes Proxy
leastbad Jun 30, 2022
67c8f50
dispatchLifecycleEvent converted to bind reflex
leastbad Jun 30, 2022
0a91fa2
top level bind doesn't work with rollup
leastbad Jul 1, 2022
d311f57
no need for finalStage concept anymore
leastbad Jul 2, 2022
a207724
added queued and delivered stages to templates
leastbad Jul 2, 2022
08d71dc
simplify lifecycle method signatures
leastbad Jul 2, 2022
4343ba0
removed action cable specific events
leastbad Jul 2, 2022
39bc7d9
simplified callbacks module
leastbad Jul 2, 2022
b463ff0
renaming reflex body param to error
leastbad Jul 2, 2022
09953b8
reworked client logging to operate on reflex params
leastbad Jul 2, 2022
9c207f6
extract duration and progress from log functions
leastbad Jul 2, 2022
a079432
pluggable transport mechanisms!
leastbad Jul 2, 2022
3844c7c
changed mode to plugin
leastbad Jul 2, 2022
bc16c5f
changed forbidden reflexes to reject promise
leastbad Jul 2, 2022
32e1e10
return error string to forbidden promise
leastbad Jul 2, 2022
5ea3d13
revert forbidden from reject to resolve
leastbad Jul 2, 2022
6b786d2
tweaks to support non-isolation mode
leastbad Jul 2, 2022
e1e4d3a
touch up comments in callback.js
leastbad Jul 2, 2022
544af22
added TODO comment wrt resolveLate
leastbad Jul 28, 2022
8eca32e
refactored reflex_store.js to reflexes.js, received() to process.js
leastbad Aug 4, 2022
5f68255
server side changes to deprecate reflex_id in favor of id
leastbad Aug 9, 2022
3c8a4b8
client side modifications to rename reflexId to id
leastbad Aug 9, 2022
1ebb204
Merge branch 'master' into refactor_reflex_store
leastbad Sep 13, 2022
baf4c08
localReflexControllers can import Stimulus
leastbad Sep 13, 2022
5f4f1a5
revert #597 馃槥
leastbad Sep 14, 2022
53bed31
added lifecycle accessor to reflex instance
leastbad Sep 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/channels/stimulus_reflex/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class StimulusReflex::Channel < StimulusReflex.configuration.parent_channel.cons
attr_reader :reflex_data

def stream_name
[params[:channel], connection.connection_identifier].join(":")
[params[:channel], connection.connection_identifier].reject(&:blank?).join(":")
end

def subscribed
Expand All @@ -25,7 +25,7 @@ def receive(data)
if reflex
reflex.rescue_with_handler(exception)
reflex.logger&.error error_message
reflex.broadcast_error data: data, body: "#{exception} #{exception.backtrace.first.split(":in ")[0] if Rails.env.development?}"
reflex.broadcast_error data: data, error: "#{exception} #{exception.backtrace.first.split(":in ")[0] if Rails.env.development?}"
else
if exception.is_a? StimulusReflex::Reflex::VersionMismatchError
mismatch = "Reflex failed due to stimulus_reflex gem/NPM package version mismatch. Package versions must match exactly.\nNote that if you are using pre-release builds, gems use the \"x.y.z.preN\" version format, while NPM packages use \"x.y.z-preN\".\n\nstimulus_reflex gem: #{StimulusReflex::VERSION}\nstimulus_reflex NPM: #{data["version"]}"
Expand All @@ -36,7 +36,7 @@ def receive(data)
CableReady::Channels.instance[stream_name].console_log(
message: mismatch,
level: "error",
reflex_id: data["reflexId"]
id: data["id"]
).broadcast
end

Expand All @@ -48,7 +48,7 @@ def receive(data)
StimulusReflex.config.logger.error error_message
end

if body.to_s.include? "No route matches"
if error_message.to_s.include? "No route matches"
initializer_path = Rails.root.join("config", "initializers", "stimulus_reflex.rb")

StimulusReflex.config.logger.warn <<~NOTE
Expand Down Expand Up @@ -82,7 +82,7 @@ def receive(data)
rescue => exception
reflex.rescue_with_handler(exception)
error = exception_with_backtrace(exception)
reflex.broadcast_error data: data, body: "#{exception} #{exception.backtrace.first.split(":in ")[0] if Rails.env.development?}"
reflex.broadcast_error data: data, error: "#{exception} #{exception.backtrace.first.split(":in ")[0] if Rails.env.development?}"
reflex.logger&.error "\e[31mReflex failed to re-render: #{error[:message]} [#{reflex_data.url}]\e[0m\n#{error[:stack]}"
end
end
Expand Down
10 changes: 10 additions & 0 deletions javascript/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
let app = {}

export default {
get app () {
return app
},
set (application) {
app = application
}
}
188 changes: 79 additions & 109 deletions javascript/callbacks.js
Original file line number Diff line number Diff line change
@@ -1,193 +1,163 @@
import CableReady from 'cable_ready'

import Log from './log'

import { reflexes } from './reflex_store'
import { reflexes } from './reflexes'
import { dispatchLifecycleEvent } from './lifecycle'
import { XPathToElement } from './utils'

const beforeDOMUpdate = event => {
const { stimulusReflex, payload } = event.detail || {}
const { stimulusReflex } = event.detail || {}
if (!stimulusReflex) return
const { reflexId, xpathElement, xpathController } = stimulusReflex
const controllerElement = XPathToElement(xpathController)
const reflexElement = XPathToElement(xpathElement)
const reflex = reflexes[reflexId]
const { promise } = reflex
const reflex = reflexes[stimulusReflex.id]

reflex.pendingOperations--

if (reflex.pendingOperations > 0) return

// TODO: remove in v4 - always resolve late
if (!stimulusReflex.resolveLate)
setTimeout(() =>
promise.resolve({
element: reflexElement,
reflex.promise.resolve({
element: reflex.element,
event,
data: promise.data,
payload,
reflexId,
data: reflex.data,
payload: reflex.payload,
id: reflex.id,
toString: () => ''
})
)
// END TODO: remove

setTimeout(() =>
dispatchLifecycleEvent(
'success',
reflexElement,
controllerElement,
reflexId,
payload
)
)
setTimeout(() => dispatchLifecycleEvent(reflex, 'success'))
}

const afterDOMUpdate = event => {
const { stimulusReflex, payload } = event.detail || {}
const { stimulusReflex } = event.detail || {}
if (!stimulusReflex) return
const { reflexId, xpathElement, xpathController } = stimulusReflex
const controllerElement = XPathToElement(xpathController)
const reflexElement = XPathToElement(xpathElement)
const reflex = reflexes[reflexId]
const { promise } = reflex
const reflex = reflexes[stimulusReflex.id]

reflex.completedOperations++
reflex.selector = event.detail.selector
reflex.morph = event.detail.stimulusReflex.morph
reflex.operation = event.type
.split(':')[1]
.split('-')
.slice(1)
.join('_')

Log.success(event, false)
Log.success(reflex)

if (reflex.completedOperations < reflex.totalOperations) return

// TODO: v4 always resolve late (remove if)
// TODO: v4 simplify to {reflex, toString}
if (stimulusReflex.resolveLate)
setTimeout(() =>
promise.resolve({
element: reflexElement,
reflex.promise.resolve({
element: reflex.element,
event,
data: promise.data,
payload,
reflexId,
data: reflex.data,
payload: reflex.payload,
id: reflex.id,
toString: () => ''
})
)

setTimeout(() =>
dispatchLifecycleEvent(
'finalize',
reflexElement,
controllerElement,
reflexId,
payload
)
)
setTimeout(() => dispatchLifecycleEvent(reflex, 'finalize'))

if (reflex.piggybackOperations.length)
CableReady.perform(reflex.piggybackOperations)
}

const routeReflexEvent = event => {
const { stimulusReflex, payload, name, body } = event.detail || {}
const { stimulusReflex, name } = event.detail || {}
const eventType = name.split('-')[2]

const eventTypes = {
nothing: nothing,
halted: halted,
forbidden: forbidden,
error: error
}
const eventTypes = { nothing, halted, forbidden, error }

if (!stimulusReflex || !Object.keys(eventTypes).includes(eventType)) return

const { reflexId, xpathElement, xpathController } = stimulusReflex
const reflexElement = XPathToElement(xpathElement)
const controllerElement = XPathToElement(xpathController)
const reflex = reflexes[reflexId]
const { promise } = reflex

if (controllerElement) {
controllerElement.reflexError = controllerElement.reflexError || {}
if (eventType === 'error') controllerElement.reflexError[reflexId] = body
}
const reflex = reflexes[stimulusReflex.id]
reflex.completedOperations++
reflex.pendingOperations--
reflex.selector = event.detail.selector
reflex.morph = event.detail.stimulusReflex.morph
reflex.operation = event.type
.split(':')[1]
.split('-')
.slice(1)
.join('_')
if (eventType === 'error') reflex.error = event.detail.error

eventTypes[eventType](event, payload, promise, reflex, reflexElement)
eventTypes[eventType](reflex, event)

setTimeout(() =>
dispatchLifecycleEvent(
eventType,
reflexElement,
controllerElement,
reflexId,
payload
)
)
setTimeout(() => dispatchLifecycleEvent(reflex, eventType))

if (reflex.piggybackOperations.length)
CableReady.perform(reflex.piggybackOperations)
}

const nothing = (event, payload, promise, reflex, reflexElement) => {
reflex.finalStage = 'after'

Log.success(event)
const nothing = (reflex, event) => {
Log.success(reflex)

// TODO: v4 simplify to {reflex, toString}
setTimeout(() =>
promise.resolve({
data: promise.data,
element: reflexElement,
reflex.promise.resolve({
data: reflex.data,
element: reflex.element,
event,
payload,
reflexId: promise.data.reflexId,
payload: reflex.payload,
id: reflex.id,
toString: () => ''
})
)
}

const halted = (event, payload, promise, reflex, reflexElement) => {
reflex.finalStage = 'halted'

Log.halted(event)
const halted = (reflex, event) => {
Log.halted(reflex, event)

// TODO: v4 simplify to {reflex, toString}
setTimeout(() =>
promise.resolve({
data: promise.data,
element: reflexElement,
reflex.promise.resolve({
data: reflex.data,
element: reflex.element,
event,
payload,
reflexId: promise.data.reflexId,
payload: reflex.payload,
id: reflex.id,
toString: () => ''
})
)
}

const forbidden = (event, payload, promise, reflex, reflexElement) => {
reflex.finalStage = 'forbidden'

Log.forbidden(event)
const forbidden = (reflex, event) => {
Log.forbidden(reflex, event)

// TODO: v4 simplify to {reflex, toString}
setTimeout(() =>
promise.resolve({
data: promise.data,
element: reflexElement,
reflex.promise.resolve({
data: reflex.data,
element: reflex.element,
event,
payload,
reflexId: promise.data.reflexId,
payload: reflex.payload,
id: reflex.id,
toString: () => ''
})
)
}

const error = (event, payload, promise, reflex, reflexElement) => {
reflex.finalStage = 'after'

Log.error(event)
const error = (reflex, event) => {
Log.error(reflex, event)

// TODO: v4 simplify to {reflex, toString}
// TODO: v4 convert to resolve?
setTimeout(() =>
promise.reject({
data: promise.data,
element: reflexElement,
reflex.promise.reject({
data: reflex.data,
element: reflex.element,
event,
payload,
reflexId: promise.data.reflexId,
error: event.detail.body,
toString: () => event.detail.body
payload: reflex.payload,
id: reflex.id,
error: reflex.error,
toString: () => reflex.error
})
)
}
Expand Down
12 changes: 8 additions & 4 deletions javascript/controllers.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import Schema from './schema'
import Stimulus from './app'

import { attributeValues } from './attributes'
import { extractReflexName } from './utils'

// Returns StimulusReflex controllers local to the passed element based on the data-controller attribute.
//
const localReflexControllers = (app, element) => {
const localReflexControllers = element => {
return attributeValues(element.getAttribute(Schema.controller)).reduce(
(memo, name) => {
const controller = app.getControllerForElementAndIdentifier(element, name)
const controller = Stimulus.app.getControllerForElementAndIdentifier(
element,
name
)
if (controller && controller.StimulusReflex) memo.push(controller)
return memo
},
Expand All @@ -19,10 +23,10 @@ const localReflexControllers = (app, element) => {
// Returns all StimulusReflex controllers for the passed element.
// Traverses DOM ancestors starting with element.
//
const allReflexControllers = (app, element) => {
const allReflexControllers = element => {
let controllers = []
while (element) {
controllers = controllers.concat(localReflexControllers(app, element))
controllers = controllers.concat(localReflexControllers(element))
element = element.parentElement
}
return controllers
Expand Down
12 changes: 12 additions & 0 deletions javascript/isolation_mode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Deprecate from './deprecate'

let isolationMode = false

export default {
Expand All @@ -6,5 +8,15 @@ export default {
},
set (value) {
isolationMode = value
if (Deprecate.enabled && !isolationMode) {
document.addEventListener(
'DOMContentLoaded',
() =>
console.warn(
'Deprecation warning: the next version of StimulusReflex will standardize isolation mode, and the isolate option will be removed.\nPlease update your applications to assume that every tab will be isolated. Use CableReady operations to broadcast updates to other tabs and users.'
),
{ once: true }
)
}
}
}