solid_objects 0.12.1 → 0.13.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/CHANGELOG.md +60 -0
- data/README.md +69 -13
- data/benchmark/idle_polling.rb +98 -0
- data/benchmark/support.rb +2 -0
- data/docs/adr/0009-realtime-updates.md +5 -1
- data/docs/adr/0011-wake-up-strategy.md +6 -0
- data/docs/architecture.md +5 -4
- data/docs/authorization.md +11 -4
- data/docs/benchmarks.md +38 -0
- data/docs/correctness.md +3 -3
- data/docs/dashboard.md +200 -0
- data/docs/database-schema.md +5 -4
- data/docs/development.md +1 -0
- data/docs/operations.md +24 -0
- data/docs/realtime.md +22 -14
- data/docs/roadmap.md +32 -11
- data/docs/security.md +7 -7
- data/examples/application/README.md +5 -5
- data/examples/application/app/actors/chat_room_actor.rb +1 -1
- data/examples/application/app/actors/shopping_cart_actor.rb +2 -2
- data/lib/solid_objects/actor.rb +1 -1
- data/lib/solid_objects/broadcast_executor.rb +35 -4
- data/lib/solid_objects/configuration.rb +4 -0
- data/lib/solid_objects/effect_executor.rb +34 -3
- data/lib/solid_objects/polling_backoff.rb +45 -0
- data/lib/solid_objects/process_registry.rb +51 -0
- data/lib/solid_objects/reminder_scheduler.rb +35 -4
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/wake_up.rb +36 -4
- data/lib/solid_objects/wake_up_adapters/postgresql.rb +6 -0
- data/lib/solid_objects/wake_up_adapters/redis.rb +25 -3
- data/lib/solid_objects/web/action.rb +109 -0
- data/lib/solid_objects/web/application.rb +239 -0
- data/lib/solid_objects/web/csrf_protection.rb +130 -0
- data/lib/solid_objects/web/helpers.rb +227 -0
- data/lib/solid_objects/web/paginator.rb +64 -0
- data/lib/solid_objects/web/route.rb +55 -0
- data/lib/solid_objects/web/router.rb +46 -0
- data/lib/solid_objects/web/statistics.rb +78 -0
- data/lib/solid_objects/web.rb +222 -0
- data/lib/solid_objects/worker.rb +39 -3
- data/lib/solid_objects.rb +2 -0
- data/sig/generated/lib/solid_objects/broadcast_executor.rbs +7 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +6 -2
- data/sig/generated/lib/solid_objects/effect_executor.rbs +7 -0
- data/sig/generated/lib/solid_objects/polling_backoff.rbs +29 -0
- data/sig/generated/lib/solid_objects/process_registry.rbs +15 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +7 -0
- data/sig/generated/lib/solid_objects/wake_up.rbs +19 -2
- data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +3 -0
- data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +17 -2
- data/sig/generated/lib/solid_objects/web/action.rbs +78 -0
- data/sig/generated/lib/solid_objects/web/application.rbs +50 -0
- data/sig/generated/lib/solid_objects/web/csrf_protection.rbs +55 -0
- data/sig/generated/lib/solid_objects/web/helpers.rbs +118 -0
- data/sig/generated/lib/solid_objects/web/paginator.rbs +54 -0
- data/sig/generated/lib/solid_objects/web/route.rbs +45 -0
- data/sig/generated/lib/solid_objects/web/router.rbs +29 -0
- data/sig/generated/lib/solid_objects/web/statistics.rbs +46 -0
- data/sig/generated/lib/solid_objects/web.rbs +129 -0
- data/sig/generated/lib/solid_objects/worker.rbs +7 -0
- data/web/assets/javascripts/application.js +118 -0
- data/web/assets/javascripts/charts.js +190 -0
- data/web/assets/stylesheets/application.css +527 -0
- data/web/views/_messages.erb +29 -0
- data/web/views/_navigation.erb +15 -0
- data/web/views/_paging.erb +17 -0
- data/web/views/_status_filter.erb +8 -0
- data/web/views/_summary.erb +39 -0
- data/web/views/broadcasts.erb +35 -0
- data/web/views/dashboard.erb +128 -0
- data/web/views/dead_letter.erb +40 -0
- data/web/views/dead_letters.erb +42 -0
- data/web/views/effects.erb +35 -0
- data/web/views/instance.erb +139 -0
- data/web/views/instances.erb +49 -0
- data/web/views/layout.erb +22 -0
- data/web/views/mailbox.erb +35 -0
- data/web/views/message.erb +34 -0
- data/web/views/processes.erb +37 -0
- data/web/views/reminders.erb +37 -0
- metadata +58 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/csrf_protection.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# A state changing request must carry the token of the session that asked
|
|
6
|
+
# for the form. The token a form receives is masked with a fresh one-time
|
|
7
|
+
# pad on every request, so the bytes on the wire differ each time and a
|
|
8
|
+
# compression side channel cannot recover the session token.
|
|
9
|
+
class CsrfProtection
|
|
10
|
+
SAFE_METHODS: untyped
|
|
11
|
+
|
|
12
|
+
TOKEN_BYTES: ::Integer
|
|
13
|
+
|
|
14
|
+
MISSING_SESSION: ::String
|
|
15
|
+
|
|
16
|
+
# @rbs (untyped) -> void
|
|
17
|
+
def initialize: (untyped) -> void
|
|
18
|
+
|
|
19
|
+
# @rbs (Hash[String, untyped]) -> Array[untyped]
|
|
20
|
+
def call: (Hash[String, untyped]) -> Array[untyped]
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# @rbs (Hash[String, untyped]) -> bool
|
|
25
|
+
def accept?: (Hash[String, untyped]) -> bool
|
|
26
|
+
|
|
27
|
+
# @rbs (Hash[String, untyped], String?) -> bool
|
|
28
|
+
def valid?: (Hash[String, untyped], String?) -> bool
|
|
29
|
+
|
|
30
|
+
# @rbs (String, String) -> bool
|
|
31
|
+
def matches?: (String, String) -> bool
|
|
32
|
+
|
|
33
|
+
# @rbs (String) -> String
|
|
34
|
+
def mask: (String) -> String
|
|
35
|
+
|
|
36
|
+
# @rbs (String) -> String
|
|
37
|
+
def unmask: (String) -> String
|
|
38
|
+
|
|
39
|
+
# @rbs (String, String) -> String
|
|
40
|
+
def exclusive_or: (String, String) -> String
|
|
41
|
+
|
|
42
|
+
# @rbs (String) -> String
|
|
43
|
+
def encode: (String) -> String
|
|
44
|
+
|
|
45
|
+
# @rbs (String) -> String?
|
|
46
|
+
def decode: (String) -> String?
|
|
47
|
+
|
|
48
|
+
# @rbs (Hash[String, untyped]) -> Hash[untyped, untyped]
|
|
49
|
+
def session!: (Hash[String, untyped]) -> Hash[untyped, untyped]
|
|
50
|
+
|
|
51
|
+
# @rbs () -> Array[untyped]
|
|
52
|
+
def forbidden: () -> Array[untyped]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/helpers.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# The methods a view may call. Everything a template prints goes through
|
|
6
|
+
# `h`, because an actor id, an operation name, and an exception message are
|
|
7
|
+
# all application supplied strings that reach this page unchanged.
|
|
8
|
+
module Helpers
|
|
9
|
+
# Only these survive a page link. A filter an operator set stays set when
|
|
10
|
+
# they turn the page; anything else the query string carries does not
|
|
11
|
+
# come back.
|
|
12
|
+
FORWARDED_PARAMS: untyped
|
|
13
|
+
|
|
14
|
+
TRUNCATION_LIMIT: ::Integer
|
|
15
|
+
|
|
16
|
+
# @rbs (untyped) -> String
|
|
17
|
+
def h: (untyped) -> String
|
|
18
|
+
|
|
19
|
+
# @rbs () -> String
|
|
20
|
+
def root_path: () -> String
|
|
21
|
+
|
|
22
|
+
# @rbs (String) -> String
|
|
23
|
+
def path_to: (String) -> String
|
|
24
|
+
|
|
25
|
+
# @rbs () -> String
|
|
26
|
+
def current_path: () -> String
|
|
27
|
+
|
|
28
|
+
# @rbs (String) -> bool
|
|
29
|
+
def current_tab?: (String) -> bool
|
|
30
|
+
|
|
31
|
+
# @rbs () -> Hash[String, String]
|
|
32
|
+
def tabs: () -> Hash[String, String]
|
|
33
|
+
|
|
34
|
+
# @rbs () -> String?
|
|
35
|
+
def csp_nonce: () -> String?
|
|
36
|
+
|
|
37
|
+
# @rbs () -> String
|
|
38
|
+
def csrf_tag: () -> String
|
|
39
|
+
|
|
40
|
+
# @rbs (String) -> String
|
|
41
|
+
def form_to: (String) -> String
|
|
42
|
+
|
|
43
|
+
# @rbs (untyped) -> String
|
|
44
|
+
def relative_time: (untyped) -> String
|
|
45
|
+
|
|
46
|
+
# @rbs (untyped) -> String
|
|
47
|
+
def number: (untyped) -> String
|
|
48
|
+
|
|
49
|
+
# @rbs (Numeric?) -> String
|
|
50
|
+
def duration: (Numeric?) -> String
|
|
51
|
+
|
|
52
|
+
# @rbs (untyped, ?Integer) -> String
|
|
53
|
+
def json_block: (untyped, ?Integer) -> String
|
|
54
|
+
|
|
55
|
+
# @rbs (String, ?Integer) -> String
|
|
56
|
+
def truncate: (String, ?Integer) -> String
|
|
57
|
+
|
|
58
|
+
# @rbs (String?) -> String
|
|
59
|
+
def status_label: (String?) -> String
|
|
60
|
+
|
|
61
|
+
# @rbs (untyped) -> String
|
|
62
|
+
def actor_label: (untyped) -> String
|
|
63
|
+
|
|
64
|
+
# @rbs (untyped) -> String
|
|
65
|
+
def instance_link: (untyped) -> String
|
|
66
|
+
|
|
67
|
+
# @rbs (?Hash[String, untyped]) -> String
|
|
68
|
+
def query_string: (?Hash[String, untyped]) -> String
|
|
69
|
+
|
|
70
|
+
# @rbs (?Hash[String, untyped]) -> String
|
|
71
|
+
def page_link: (?Hash[String, untyped]) -> String
|
|
72
|
+
|
|
73
|
+
# @rbs (Instance) -> String
|
|
74
|
+
def lease_state: (Instance) -> String
|
|
75
|
+
|
|
76
|
+
# @rbs () -> Statistics
|
|
77
|
+
def statistics: () -> Statistics
|
|
78
|
+
|
|
79
|
+
# @rbs (untyped) -> Paginator
|
|
80
|
+
def paginate: (untyped) -> Paginator
|
|
81
|
+
|
|
82
|
+
# An unrecognized filter falls back to the default rather than returning
|
|
83
|
+
# nothing, so a hand edited query string cannot make a page look empty.
|
|
84
|
+
# @rbs (Array[String], ?default: String?) -> String?
|
|
85
|
+
def filter_value: (Array[String], ?default: String?) -> String?
|
|
86
|
+
|
|
87
|
+
# @rbs () -> Instance
|
|
88
|
+
def find_instance: () -> Instance
|
|
89
|
+
|
|
90
|
+
# @rbs () -> untyped
|
|
91
|
+
def filtered_instances: () -> untyped
|
|
92
|
+
|
|
93
|
+
# @rbs (String) -> untyped
|
|
94
|
+
def mailbox_messages: (String) -> untyped
|
|
95
|
+
|
|
96
|
+
# Chart data travels in an attribute rather than an inline script block,
|
|
97
|
+
# so the page needs no script-src exception and an actor type cannot
|
|
98
|
+
# close the attribute and open a tag.
|
|
99
|
+
#
|
|
100
|
+
# The container is not decoration. Chart.js measures a responsive canvas
|
|
101
|
+
# against its parent, so the parent has to have a height of its own; a
|
|
102
|
+
# panel that sizes to its children would grow a little on every redraw.
|
|
103
|
+
# @rbs (String, untyped) -> String
|
|
104
|
+
def chart: (String, untyped) -> String
|
|
105
|
+
|
|
106
|
+
# A vendored copy is a path below the mount; a CDN copy is an absolute
|
|
107
|
+
# URL and is left alone.
|
|
108
|
+
# @rbs () -> String
|
|
109
|
+
def chart_library_source: () -> String
|
|
110
|
+
|
|
111
|
+
# @rbs () -> String
|
|
112
|
+
def chart_library_integrity_attributes: () -> String
|
|
113
|
+
|
|
114
|
+
# @rbs () -> String
|
|
115
|
+
def environment_name: () -> String
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/paginator.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# Counts and slices one relation. The page size is clamped because the page
|
|
6
|
+
# number and the page size both arrive from the query string, and an
|
|
7
|
+
# operator page that accepts an unbounded limit is a denial of service
|
|
8
|
+
# against the database the actors run on.
|
|
9
|
+
class Paginator
|
|
10
|
+
DEFAULT_PER_PAGE: ::Integer
|
|
11
|
+
|
|
12
|
+
MAXIMUM_PER_PAGE: ::Integer
|
|
13
|
+
|
|
14
|
+
@page: Integer
|
|
15
|
+
|
|
16
|
+
@per_page: Integer
|
|
17
|
+
|
|
18
|
+
@total: Integer
|
|
19
|
+
|
|
20
|
+
@records: Array[untyped]
|
|
21
|
+
|
|
22
|
+
attr_reader page: untyped
|
|
23
|
+
|
|
24
|
+
attr_reader per_page: untyped
|
|
25
|
+
|
|
26
|
+
attr_reader total: untyped
|
|
27
|
+
|
|
28
|
+
attr_reader records: untyped
|
|
29
|
+
|
|
30
|
+
# @rbs (relation: untyped, ?page: String?, ?per_page: String?) -> void
|
|
31
|
+
def initialize: (relation: untyped, ?page: String?, ?per_page: String?) -> void
|
|
32
|
+
|
|
33
|
+
# @rbs () -> Integer
|
|
34
|
+
def last_page: () -> Integer
|
|
35
|
+
|
|
36
|
+
# @rbs () -> Integer?
|
|
37
|
+
def previous_page: () -> Integer?
|
|
38
|
+
|
|
39
|
+
# @rbs () -> Integer?
|
|
40
|
+
def next_page: () -> Integer?
|
|
41
|
+
|
|
42
|
+
# @rbs () -> Integer
|
|
43
|
+
def first_record: () -> Integer
|
|
44
|
+
|
|
45
|
+
# @rbs () -> Integer
|
|
46
|
+
def last_record: () -> Integer
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @rbs (String?, default: Integer, maximum: Integer) -> Integer
|
|
51
|
+
def bounded: (String?, default: Integer, maximum: Integer) -> Integer
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/route.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
class Route
|
|
6
|
+
# A named segment stops at the next slash, so `/instances/:id` never
|
|
7
|
+
# swallows `/instances/1/pause` and route order cannot hide a page.
|
|
8
|
+
NAMED_SEGMENT: ::Regexp
|
|
9
|
+
|
|
10
|
+
SEGMENT_CAPTURE: ::String
|
|
11
|
+
|
|
12
|
+
@matcher: String | Regexp
|
|
13
|
+
|
|
14
|
+
@request_method: String
|
|
15
|
+
|
|
16
|
+
@pattern: String
|
|
17
|
+
|
|
18
|
+
@policy: Hash[Symbol, String]
|
|
19
|
+
|
|
20
|
+
@handler: Proc
|
|
21
|
+
|
|
22
|
+
attr_reader request_method: untyped
|
|
23
|
+
|
|
24
|
+
attr_reader pattern: untyped
|
|
25
|
+
|
|
26
|
+
attr_reader policy: untyped
|
|
27
|
+
|
|
28
|
+
attr_reader handler: untyped
|
|
29
|
+
|
|
30
|
+
# @rbs (request_method: String, pattern: String, policy: Hash[Symbol, String], handler: Proc) -> void
|
|
31
|
+
def initialize: (request_method: String, pattern: String, policy: Hash[Symbol, String], handler: Proc) -> void
|
|
32
|
+
|
|
33
|
+
# @rbs (String) -> bool
|
|
34
|
+
def match?: (String) -> bool
|
|
35
|
+
|
|
36
|
+
# @rbs (String) -> Hash[Symbol, String?]
|
|
37
|
+
def capture: (String) -> Hash[Symbol, String?]
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @rbs (String) -> (String | Regexp)
|
|
42
|
+
def compile: (String) -> (String | Regexp)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/router.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# Declares the pages of the dashboard. Every route carries the
|
|
6
|
+
# administration policy it needs, and a route declared without one raises
|
|
7
|
+
# at load time. A new page therefore cannot reach the database before an
|
|
8
|
+
# application has said who may read it.
|
|
9
|
+
module Router
|
|
10
|
+
# @rbs (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
11
|
+
def head: (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
12
|
+
|
|
13
|
+
# @rbs (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
14
|
+
def get: (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
15
|
+
|
|
16
|
+
# @rbs (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
17
|
+
def post: (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
18
|
+
|
|
19
|
+
# @rbs (String, String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
20
|
+
def route: (String, String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
|
|
21
|
+
|
|
22
|
+
# @rbs () -> Hash[String, Array[Route]]
|
|
23
|
+
def routes: () -> Hash[String, Array[Route]]
|
|
24
|
+
|
|
25
|
+
# @rbs (String, String) -> Route?
|
|
26
|
+
def match: (String, String) -> Route?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web/statistics.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Web
|
|
5
|
+
# The counts behind the dashboard and behind `GET /stats`. Both read the
|
|
6
|
+
# same object, so the polled JSON and the rendered page can never disagree
|
|
7
|
+
# about what a number means.
|
|
8
|
+
class Statistics
|
|
9
|
+
EFFECT_STATUSES: untyped
|
|
10
|
+
|
|
11
|
+
BROADCAST_STATUSES: untyped
|
|
12
|
+
|
|
13
|
+
REMINDER_STATUSES: untyped
|
|
14
|
+
|
|
15
|
+
PROCESS_STATES: untyped
|
|
16
|
+
|
|
17
|
+
@now: Time
|
|
18
|
+
|
|
19
|
+
attr_reader now: untyped
|
|
20
|
+
|
|
21
|
+
# @rbs (?now: Time) -> void
|
|
22
|
+
def initialize: (?now: Time) -> void
|
|
23
|
+
|
|
24
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
25
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
26
|
+
|
|
27
|
+
# @rbs () -> Hash[Symbol, Integer]
|
|
28
|
+
def instances: () -> Hash[Symbol, Integer]
|
|
29
|
+
|
|
30
|
+
# The oldest ready message that is already due is the queue latency of
|
|
31
|
+
# this runtime: how far behind the workers are, in seconds.
|
|
32
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
33
|
+
def mailbox: () -> Hash[Symbol, untyped]
|
|
34
|
+
|
|
35
|
+
# @rbs () -> Hash[Symbol, Integer]
|
|
36
|
+
def reminders: () -> Hash[Symbol, Integer]
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# A status the schema allows but the table does not currently hold still
|
|
41
|
+
# reports zero, so a row of counts keeps the same shape between polls.
|
|
42
|
+
# @rbs (untyped, Symbol, Array[String]) -> Hash[Symbol, Integer]
|
|
43
|
+
def grouped: (untyped, Symbol, Array[String]) -> Hash[Symbol, Integer]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/web.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
# An operator dashboard for the actor runtime, served as a Rack application:
|
|
5
|
+
#
|
|
6
|
+
# Rails.application.routes.draw do
|
|
7
|
+
# mount SolidObjects::Web => "/solid_objects"
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# It is deliberately not loaded by `require "solid_objects"`. A worker
|
|
11
|
+
# process must not carry a web stack, and an application that never mounts
|
|
12
|
+
# the dashboard must not pay for it.
|
|
13
|
+
#
|
|
14
|
+
# Every page asks `configuration.authorize_administration` first. That block
|
|
15
|
+
# denies by default, so a mount alone exposes nothing until an application
|
|
16
|
+
# states who may read it.
|
|
17
|
+
class Web
|
|
18
|
+
ROOT: untyped
|
|
19
|
+
|
|
20
|
+
VIEWS: untyped
|
|
21
|
+
|
|
22
|
+
ASSETS: untyped
|
|
23
|
+
|
|
24
|
+
NONCE_KEY: ::String
|
|
25
|
+
|
|
26
|
+
CSRF_TOKEN_KEY: ::String
|
|
27
|
+
|
|
28
|
+
NONCE_BYTES: ::Integer
|
|
29
|
+
|
|
30
|
+
ASSET_CACHE_SECONDS: ::Integer
|
|
31
|
+
|
|
32
|
+
TEMPLATE_NAME: ::Regexp
|
|
33
|
+
|
|
34
|
+
# The dashboard charts need a charting library, and this one is fetched
|
|
35
|
+
# from a public CDN with a subresource integrity hash, so a compromised CDN
|
|
36
|
+
# cannot substitute other code. A deployment with no outbound network
|
|
37
|
+
# access should vendor the file and point `chart_library_url` at it, or set
|
|
38
|
+
# that to nil to render the dashboard without charts.
|
|
39
|
+
CHART_LIBRARY_URL: ::String
|
|
40
|
+
|
|
41
|
+
CHART_LIBRARY_INTEGRITY: ::String
|
|
42
|
+
|
|
43
|
+
DEFAULT_TABS: untyped
|
|
44
|
+
|
|
45
|
+
LOCK: untyped
|
|
46
|
+
|
|
47
|
+
# A path below the mount serves a vendored copy; an absolute URL is
|
|
48
|
+
# fetched from that host and is named in the content security policy.
|
|
49
|
+
# nil renders the dashboard without charts.
|
|
50
|
+
# @rbs @chart_library_url: String?
|
|
51
|
+
# @rbs @chart_library_integrity: String?
|
|
52
|
+
attr_writer chart_library_url: untyped
|
|
53
|
+
|
|
54
|
+
# A path below the mount serves a vendored copy; an absolute URL is
|
|
55
|
+
# fetched from that host and is named in the content security policy.
|
|
56
|
+
# nil renders the dashboard without charts.
|
|
57
|
+
# @rbs @chart_library_url: String?
|
|
58
|
+
# @rbs @chart_library_integrity: String?
|
|
59
|
+
attr_writer chart_library_integrity: untyped
|
|
60
|
+
|
|
61
|
+
# @rbs () -> String?
|
|
62
|
+
def self.chart_library_url: () -> String?
|
|
63
|
+
|
|
64
|
+
# @rbs () -> String?
|
|
65
|
+
def self.chart_library_integrity: () -> String?
|
|
66
|
+
|
|
67
|
+
# @rbs () -> bool
|
|
68
|
+
def self.charts?: () -> bool
|
|
69
|
+
|
|
70
|
+
# The origin the content security policy has to allow. A vendored copy
|
|
71
|
+
# served from the mount has none, so the policy stays at 'self'.
|
|
72
|
+
# @rbs () -> String?
|
|
73
|
+
def self.chart_library_origin: () -> String?
|
|
74
|
+
|
|
75
|
+
# @rbs () -> Hash[String, String]
|
|
76
|
+
def self.tabs: () -> Hash[String, String]
|
|
77
|
+
|
|
78
|
+
# Searched in order, so an extension directory added first wins over the
|
|
79
|
+
# packaged one and an application can replace a single page.
|
|
80
|
+
# @rbs () -> Array[String]
|
|
81
|
+
def self.views: () -> Array[String]
|
|
82
|
+
|
|
83
|
+
# @rbs () -> Array[[Array[untyped], Proc?]]
|
|
84
|
+
def self.middlewares: () -> Array[[ Array[untyped], Proc? ]]
|
|
85
|
+
|
|
86
|
+
# The built stack is memoized, so a middleware added after the first
|
|
87
|
+
# request would otherwise be dropped without a word.
|
|
88
|
+
# @rbs (*untyped) ?{ () -> untyped } -> void
|
|
89
|
+
def self.use: (*untyped) ?{ () -> untyped } -> void
|
|
90
|
+
|
|
91
|
+
# Adds pages to the dashboard. The extension receives the application
|
|
92
|
+
# class and declares its own routes on it, which means its routes carry
|
|
93
|
+
# an authorization policy like every other route.
|
|
94
|
+
#
|
|
95
|
+
# @rbs (untyped, tab: String, path: String, ?views: String?) -> void
|
|
96
|
+
def self.register: (untyped, tab: String, path: String, ?views: String?) -> void
|
|
97
|
+
|
|
98
|
+
# @rbs (Symbol) -> untyped
|
|
99
|
+
def self.template: (Symbol) -> untyped
|
|
100
|
+
|
|
101
|
+
# @rbs () -> void
|
|
102
|
+
def self.reset!: () -> void
|
|
103
|
+
|
|
104
|
+
# @rbs (Hash[String, untyped]) -> Array[untyped]
|
|
105
|
+
def self.call: (Hash[String, untyped]) -> Array[untyped]
|
|
106
|
+
|
|
107
|
+
# @rbs () -> Web
|
|
108
|
+
def self.application: () -> Web
|
|
109
|
+
|
|
110
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
111
|
+
private def self.templates: () -> Hash[Symbol, untyped]
|
|
112
|
+
|
|
113
|
+
# A template name never comes from a request, and this keeps it that way
|
|
114
|
+
# rather than trusting every future caller to know it.
|
|
115
|
+
# @rbs (Symbol) -> String
|
|
116
|
+
private def self.template_path: (Symbol) -> String
|
|
117
|
+
|
|
118
|
+
# @rbs (Hash[String, untyped]) -> Array[untyped]
|
|
119
|
+
def call: (Hash[String, untyped]) -> Array[untyped]
|
|
120
|
+
|
|
121
|
+
# @rbs () -> untyped
|
|
122
|
+
def app: () -> untyped
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# @rbs () -> untyped
|
|
127
|
+
def build: () -> untyped
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class Worker
|
|
5
|
+
@polling_backoff: PollingBackoff
|
|
6
|
+
|
|
5
7
|
@shutdown_requested: bool
|
|
6
8
|
|
|
7
9
|
@stopped: bool
|
|
@@ -36,6 +38,9 @@ module SolidObjects
|
|
|
36
38
|
# @rbs () -> bool
|
|
37
39
|
def shutdown_requested?: () -> bool
|
|
38
40
|
|
|
41
|
+
# @rbs () -> Float
|
|
42
|
+
def current_polling_interval: () -> Float
|
|
43
|
+
|
|
39
44
|
private
|
|
40
45
|
|
|
41
46
|
attr_reader process_registry: untyped
|
|
@@ -44,6 +49,8 @@ module SolidObjects
|
|
|
44
49
|
|
|
45
50
|
attr_reader activations: untyped
|
|
46
51
|
|
|
52
|
+
attr_reader polling_backoff: untyped
|
|
53
|
+
|
|
47
54
|
# @rbs () -> Activation?
|
|
48
55
|
def cached_ready_activation: () -> Activation?
|
|
49
56
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Refreshes the summary bar in place. Nothing else on the page polls, because
|
|
2
|
+
// a dashboard that reloads a table an operator is reading is worse than one
|
|
3
|
+
// that shows a stale table with a live total.
|
|
4
|
+
(function () {
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
var POLL_INTERVAL = 5000;
|
|
8
|
+
var STORAGE_KEY = "solid-objects-live";
|
|
9
|
+
|
|
10
|
+
var timer = null;
|
|
11
|
+
|
|
12
|
+
function readPath() {
|
|
13
|
+
return document.body.getAttribute("data-stats-path");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function lookup(payload, path) {
|
|
17
|
+
return path.split(".").reduce(function (value, key) {
|
|
18
|
+
return value == null ? null : value[key];
|
|
19
|
+
}, payload);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Must match SolidObjects::Web::Helpers#number and #duration. A polled cell
|
|
23
|
+
// sits beside server-rendered ones, and a value that changes shape the
|
|
24
|
+
// moment polling starts reads as a different measurement.
|
|
25
|
+
//
|
|
26
|
+
// The cell declares which one it is. A duration of exactly zero arrives from
|
|
27
|
+
// JSON as an integer-valued number, so guessing from the value would print
|
|
28
|
+
// "0" where the server printed "0.000 s".
|
|
29
|
+
function format(value, kind) {
|
|
30
|
+
if (typeof value !== "number") return String(value);
|
|
31
|
+
if (kind !== "duration") return value.toLocaleString("en-US");
|
|
32
|
+
if (value < 60) return value.toFixed(3) + " s";
|
|
33
|
+
|
|
34
|
+
return Math.floor(value / 60) + " min " + Math.round(value % 60) + " s";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function apply(payload) {
|
|
38
|
+
var cells = document.querySelectorAll("[data-statistic]");
|
|
39
|
+
Array.prototype.forEach.call(cells, function (cell) {
|
|
40
|
+
var value = lookup(payload, cell.getAttribute("data-statistic"));
|
|
41
|
+
if (value == null) return;
|
|
42
|
+
|
|
43
|
+
var text = format(value, cell.getAttribute("data-format"));
|
|
44
|
+
if (cell.textContent !== text) cell.textContent = text;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function poll() {
|
|
49
|
+
var path = readPath();
|
|
50
|
+
if (!path) return;
|
|
51
|
+
|
|
52
|
+
fetch(path, { credentials: "same-origin", headers: { Accept: "application/json" } })
|
|
53
|
+
.then(function (response) {
|
|
54
|
+
return response.ok ? response.json() : null;
|
|
55
|
+
})
|
|
56
|
+
.then(function (payload) {
|
|
57
|
+
if (!payload) return;
|
|
58
|
+
|
|
59
|
+
apply(payload);
|
|
60
|
+
// The charts subscribe to this rather than polling on their own, so
|
|
61
|
+
// one request refreshes every number on the page.
|
|
62
|
+
document.dispatchEvent(
|
|
63
|
+
new CustomEvent("solid-objects:statistics", { detail: payload })
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
.catch(function () {
|
|
67
|
+
// A failed poll leaves the last rendered totals in place.
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setLive(button, live) {
|
|
72
|
+
button.setAttribute("aria-pressed", live ? "true" : "false");
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
window.localStorage.setItem(STORAGE_KEY, live ? "on" : "off");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Storage is unavailable in private windows; the toggle still works.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (timer) {
|
|
81
|
+
window.clearInterval(timer);
|
|
82
|
+
timer = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (live) {
|
|
86
|
+
poll();
|
|
87
|
+
timer = window.setInterval(poll, POLL_INTERVAL);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stored() {
|
|
92
|
+
try {
|
|
93
|
+
return window.localStorage.getItem(STORAGE_KEY) === "on";
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function start() {
|
|
100
|
+
var button = document.querySelector("[data-poll-toggle]");
|
|
101
|
+
if (!button) return;
|
|
102
|
+
|
|
103
|
+
button.addEventListener("click", function () {
|
|
104
|
+
setLive(button, button.getAttribute("aria-pressed") !== "true");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (stored()) setLive(button, true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The tag is deferred, so the document is usually still parsing. A script
|
|
111
|
+
// evaluated after the document finished would never see the event, and
|
|
112
|
+
// waiting for one that already fired leaves the toggle inert.
|
|
113
|
+
if (document.readyState === "loading") {
|
|
114
|
+
document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
115
|
+
} else {
|
|
116
|
+
start();
|
|
117
|
+
}
|
|
118
|
+
})();
|