textus 0.26.0 → 0.29.0
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/ARCHITECTURE.md +111 -67
- data/CHANGELOG.md +76 -0
- data/README.md +55 -13
- data/SPEC.md +75 -38
- data/docs/conventions.md +4 -4
- data/lib/textus/boot.rb +14 -10
- data/lib/textus/builder/pipeline.rb +13 -12
- data/lib/textus/call.rb +28 -0
- data/lib/textus/cli/verb/audit.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +2 -2
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +2 -2
- data/lib/textus/cli/verb/put.rb +3 -3
- data/lib/textus/cli/verb.rb +6 -6
- data/lib/textus/cli.rb +0 -7
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +49 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +10 -8
- data/lib/textus/doctor/check.rb +12 -5
- data/lib/textus/doctor.rb +7 -7
- data/lib/textus/domain/authorizer.rb +2 -2
- data/lib/textus/domain/sentinel.rb +9 -65
- data/lib/textus/domain/staleness/generator_check.rb +46 -26
- data/lib/textus/domain/staleness/intake_check.rb +18 -10
- data/lib/textus/domain/staleness.rb +3 -3
- data/lib/textus/{application/envelope → envelope/io}/reader.rb +2 -2
- data/lib/textus/{application/envelope → envelope/io}/writer.rb +11 -11
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/rpc_registry.rb +1 -1
- data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
- data/lib/textus/maintenance/migrate.rb +51 -0
- data/lib/textus/maintenance/rule_lint.rb +56 -0
- data/lib/textus/maintenance/zone_mv.rb +51 -0
- data/lib/textus/maintenance.rb +15 -0
- data/lib/textus/manifest/data.rb +4 -3
- data/lib/textus/manifest/entry/base.rb +38 -18
- data/lib/textus/manifest/entry/derived.rb +6 -6
- data/lib/textus/manifest/entry/nested.rb +7 -9
- data/lib/textus/manifest/entry/parser.rb +2 -2
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
- data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
- data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +2 -2
- data/lib/textus/manifest/entry.rb +0 -5
- data/lib/textus/manifest.rb +1 -6
- data/lib/textus/mcp/server.rb +1 -2
- data/lib/textus/mcp/session.rb +10 -1
- data/lib/textus/mcp/tools.rb +2 -2
- data/lib/textus/mcp.rb +1 -1
- data/lib/textus/{infra → ports}/audit_log.rb +1 -1
- data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
- data/lib/textus/{infra → ports}/build_lock.rb +1 -1
- data/lib/textus/{infra → ports}/clock.rb +1 -1
- data/lib/textus/{infra → ports}/publisher.rb +6 -6
- data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
- data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +67 -0
- data/lib/textus/ports/storage/file_stat.rb +19 -0
- data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
- data/lib/textus/projection.rb +91 -0
- data/lib/textus/read/audit.rb +111 -0
- data/lib/textus/read/blame.rb +81 -0
- data/lib/textus/read/boot.rb +18 -0
- data/lib/textus/read/deps.rb +24 -0
- data/lib/textus/read/doctor.rb +19 -0
- data/lib/textus/read/freshness.rb +101 -0
- data/lib/textus/read/get.rb +66 -0
- data/lib/textus/read/get_or_refresh.rb +69 -0
- data/lib/textus/read/list.rb +15 -0
- data/lib/textus/read/policy_explain.rb +37 -0
- data/lib/textus/read/published.rb +15 -0
- data/lib/textus/read/pulse.rb +89 -0
- data/lib/textus/read/rdeps.rb +25 -0
- data/lib/textus/read/schema_envelope.rb +16 -0
- data/lib/textus/read/stale.rb +17 -0
- data/lib/textus/read/uid.rb +20 -0
- data/lib/textus/read/validate_all.rb +22 -0
- data/lib/textus/read/validator.rb +84 -0
- data/lib/textus/read/where.rb +16 -0
- data/lib/textus/role_scope.rb +49 -0
- data/lib/textus/schema/tools.rb +3 -3
- data/lib/textus/store.rb +16 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +86 -0
- data/lib/textus/write/authority_gate.rb +24 -0
- data/lib/textus/write/delete.rb +54 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +123 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +59 -0
- data/lib/textus/write/refresh_all.rb +44 -0
- data/lib/textus/write/refresh_orchestrator.rb +102 -0
- data/lib/textus/write/refresh_worker.rb +138 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus.rb +1 -2
- metadata +54 -50
- data/lib/textus/application/caps.rb +0 -49
- data/lib/textus/application/context.rb +0 -34
- data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
- data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
- data/lib/textus/application/maintenance/migrate.rb +0 -59
- data/lib/textus/application/maintenance/rule_lint.rb +0 -65
- data/lib/textus/application/maintenance/zone_mv.rb +0 -60
- data/lib/textus/application/maintenance.rb +0 -17
- data/lib/textus/application/projection.rb +0 -93
- data/lib/textus/application/read/audit.rb +0 -106
- data/lib/textus/application/read/blame.rb +0 -91
- data/lib/textus/application/read/deps.rb +0 -34
- data/lib/textus/application/read/freshness.rb +0 -110
- data/lib/textus/application/read/get.rb +0 -75
- data/lib/textus/application/read/get_or_refresh.rb +0 -63
- data/lib/textus/application/read/list.rb +0 -25
- data/lib/textus/application/read/policy_explain.rb +0 -47
- data/lib/textus/application/read/published.rb +0 -25
- data/lib/textus/application/read/pulse.rb +0 -101
- data/lib/textus/application/read/rdeps.rb +0 -35
- data/lib/textus/application/read/schema_envelope.rb +0 -26
- data/lib/textus/application/read/stale.rb +0 -23
- data/lib/textus/application/read/uid.rb +0 -30
- data/lib/textus/application/read/validate_all.rb +0 -32
- data/lib/textus/application/read/validator.rb +0 -86
- data/lib/textus/application/read/where.rb +0 -26
- data/lib/textus/application/use_case.rb +0 -22
- data/lib/textus/application/write/accept.rb +0 -102
- data/lib/textus/application/write/authority_gate.rb +0 -26
- data/lib/textus/application/write/delete.rb +0 -45
- data/lib/textus/application/write/materializer.rb +0 -49
- data/lib/textus/application/write/mv.rb +0 -118
- data/lib/textus/application/write/publish.rb +0 -96
- data/lib/textus/application/write/put.rb +0 -49
- data/lib/textus/application/write/refresh_all.rb +0 -63
- data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
- data/lib/textus/application/write/refresh_worker.rb +0 -134
- data/lib/textus/application/write/reject.rb +0 -62
- data/lib/textus/session.rb +0 -84
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Write
|
|
4
|
-
# Single-pass publish use case: dispatches polymorphically to each
|
|
5
|
-
# entry's `publish_via` method. Derived entries materialize their body
|
|
6
|
-
# via Materializer; Nested entries fan out via publish_each; Leaf and
|
|
7
|
-
# Intake entries copy their stored body to publish_to targets. The
|
|
8
|
-
# Publish layer owns wiring (context, accumulation) but not per-kind
|
|
9
|
-
# logic.
|
|
10
|
-
#
|
|
11
|
-
# Return shape: { "protocol", "built", "published_leaves" }
|
|
12
|
-
module Publish
|
|
13
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
14
|
-
Impl.new(
|
|
15
|
-
ctx: ctx, caps: caps,
|
|
16
|
-
rpc: session.rpc,
|
|
17
|
-
session: session,
|
|
18
|
-
hook_context: session.hook_context
|
|
19
|
-
).call(*, **)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
class Impl
|
|
23
|
-
def initialize(ctx:, caps:, rpc:, session:, hook_context:)
|
|
24
|
-
@ctx = ctx
|
|
25
|
-
@caps = caps
|
|
26
|
-
@manifest = caps.manifest
|
|
27
|
-
@file_store = caps.file_store
|
|
28
|
-
@events = caps.events
|
|
29
|
-
@root = caps.root
|
|
30
|
-
@rpc = rpc
|
|
31
|
-
@session = session
|
|
32
|
-
@hook_context = hook_context
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def call(prefix: nil)
|
|
36
|
-
built = []
|
|
37
|
-
leaves = []
|
|
38
|
-
context = build_context
|
|
39
|
-
|
|
40
|
-
@manifest.data.entries.each do |mentry|
|
|
41
|
-
next if prefix && !entry_matches_prefix?(mentry, prefix)
|
|
42
|
-
|
|
43
|
-
result = mentry.publish_via(context, prefix: prefix)
|
|
44
|
-
next if result.nil?
|
|
45
|
-
|
|
46
|
-
case result[:kind]
|
|
47
|
-
when :built then built << result[:value]
|
|
48
|
-
when :leaves then leaves.concat(result[:value])
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
{ "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => leaves }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def build_context
|
|
58
|
-
Textus::Manifest::Entry::Base::PublishContext.new(
|
|
59
|
-
repo_root: File.dirname(@root),
|
|
60
|
-
manifest: @manifest,
|
|
61
|
-
file_store: @file_store,
|
|
62
|
-
root: @root,
|
|
63
|
-
caps: @caps,
|
|
64
|
-
rpc: @rpc,
|
|
65
|
-
session: @session,
|
|
66
|
-
ctx: @ctx,
|
|
67
|
-
bus: @events,
|
|
68
|
-
hook_context: @hook_context,
|
|
69
|
-
reader: reader,
|
|
70
|
-
emit: ->(event, **payload) { @events.publish(event, ctx: @hook_context, **payload) },
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Whether the entry should be processed for the given prefix filter.
|
|
75
|
-
def entry_matches_prefix?(mentry, prefix)
|
|
76
|
-
return true unless prefix
|
|
77
|
-
|
|
78
|
-
case mentry
|
|
79
|
-
when Textus::Manifest::Entry::Nested
|
|
80
|
-
mentry.key.start_with?(prefix) ||
|
|
81
|
-
prefix.start_with?("#{mentry.key}.")
|
|
82
|
-
else
|
|
83
|
-
mentry.key.start_with?(prefix)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def reader
|
|
88
|
-
@reader ||= Textus::Application::Read::Get::Impl.new(ctx: @ctx, caps: @caps)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
Textus::Application::UseCase.register(:publish, Textus::Application::Write::Publish, caps: :write)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Write
|
|
4
|
-
module Put
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
6
|
-
Impl.new(
|
|
7
|
-
ctx: ctx, caps: caps,
|
|
8
|
-
writer: session.envelope_writer,
|
|
9
|
-
hook_context: session.hook_context
|
|
10
|
-
).call(*, **)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class Impl
|
|
14
|
-
def initialize(ctx:, caps:, writer:, hook_context:)
|
|
15
|
-
@ctx = ctx
|
|
16
|
-
@manifest = caps.manifest
|
|
17
|
-
@events = caps.events
|
|
18
|
-
@authorizer = caps.authorizer
|
|
19
|
-
@writer = writer
|
|
20
|
-
@hook_context = hook_context
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
24
|
-
Textus::Manifest::Data.validate_key!(key)
|
|
25
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
26
|
-
|
|
27
|
-
@authorizer.authorize_write!(mentry, role: @ctx.role)
|
|
28
|
-
|
|
29
|
-
envelope = @writer.put(
|
|
30
|
-
key,
|
|
31
|
-
mentry: mentry,
|
|
32
|
-
payload: Textus::Application::Envelope::Writer::Payload.new(meta: meta, body: body, content: content),
|
|
33
|
-
if_etag: if_etag,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
@events.publish(:entry_put,
|
|
37
|
-
ctx: @hook_context,
|
|
38
|
-
key: key,
|
|
39
|
-
envelope: envelope)
|
|
40
|
-
|
|
41
|
-
envelope
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
Textus::Application::UseCase.register(:put, Textus::Application::Write::Put, caps: :write)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Write
|
|
4
|
-
module RefreshAll
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
6
|
-
Impl.new(
|
|
7
|
-
ctx: ctx, caps: caps,
|
|
8
|
-
rpc: session.rpc,
|
|
9
|
-
writer: session.envelope_writer,
|
|
10
|
-
hook_context: session.hook_context
|
|
11
|
-
).call(*, **)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class Impl
|
|
15
|
-
def initialize(ctx:, caps:, rpc:, writer:, hook_context:)
|
|
16
|
-
@ctx = ctx
|
|
17
|
-
@caps = caps
|
|
18
|
-
@rpc = rpc
|
|
19
|
-
@writer = writer
|
|
20
|
-
@hook_context = hook_context
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(prefix: nil, zone: nil)
|
|
24
|
-
worker = Textus::Application::Write::RefreshWorker::Impl.new(
|
|
25
|
-
ctx: @ctx, caps: @caps, rpc: @rpc, writer: @writer,
|
|
26
|
-
hook_context: @hook_context
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
stale_rows = Textus::Application::Read::Stale::Impl.new(caps: @caps).call(prefix: prefix, zone: zone)
|
|
30
|
-
refreshed = []
|
|
31
|
-
failed = []
|
|
32
|
-
skipped = []
|
|
33
|
-
|
|
34
|
-
stale_rows.each do |row|
|
|
35
|
-
key = row["key"] || row[:key]
|
|
36
|
-
reason = row["reason"] || row[:reason]
|
|
37
|
-
if reason.to_s.match?(/ttl exceeded|never refreshed/)
|
|
38
|
-
begin
|
|
39
|
-
worker.run(key)
|
|
40
|
-
refreshed << key
|
|
41
|
-
rescue Textus::Error => e
|
|
42
|
-
failed << { "key" => key, "error" => e.message }
|
|
43
|
-
end
|
|
44
|
-
else
|
|
45
|
-
skipped << { "key" => key, "reason" => reason }
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
"protocol" => Textus::PROTOCOL,
|
|
51
|
-
"ok" => failed.empty?,
|
|
52
|
-
"refreshed" => refreshed,
|
|
53
|
-
"failed" => failed,
|
|
54
|
-
"skipped" => skipped,
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
Textus::Application::UseCase.register(:refresh_all, Textus::Application::Write::RefreshAll, caps: :write)
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Write
|
|
4
|
-
class RefreshOrchestrator
|
|
5
|
-
def initialize(worker:, store_root:, events:, ctx: nil, hook_context: nil, detached_spawner: nil)
|
|
6
|
-
@worker = worker
|
|
7
|
-
@store_root = store_root
|
|
8
|
-
@events = events
|
|
9
|
-
@ctx = ctx
|
|
10
|
-
@hook_context = hook_context
|
|
11
|
-
@detached_spawner = detached_spawner || default_spawner
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def execute(action, key:)
|
|
15
|
-
case action
|
|
16
|
-
when Textus::Domain::Action::Return then Textus::Domain::Outcome::Skipped.new
|
|
17
|
-
when Textus::Domain::Action::RefreshSync then run_sync(key)
|
|
18
|
-
when Textus::Domain::Action::RefreshTimed then run_timed(action.budget_ms, key)
|
|
19
|
-
else raise ArgumentError.new("unknown action: #{action.inspect}")
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def run_sync(key)
|
|
26
|
-
envelope = @worker.run(key)
|
|
27
|
-
Textus::Domain::Outcome::Refreshed.new(envelope: envelope)
|
|
28
|
-
rescue Textus::Error => e
|
|
29
|
-
Textus::Domain::Outcome::Failed.new(error: e)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def run_timed(budget_ms, key)
|
|
33
|
-
return run_timed_with_fork(budget_ms, key) if Textus::Infra::Refresh::Detached.supported?
|
|
34
|
-
|
|
35
|
-
run_timed_cooperative(budget_ms, key)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def run_timed_cooperative(budget_ms, key)
|
|
39
|
-
result = nil
|
|
40
|
-
thread = Thread.new do
|
|
41
|
-
result = @worker.run(key)
|
|
42
|
-
rescue Textus::Error => e
|
|
43
|
-
result = e
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
thread.join(budget_ms / 1000.0)
|
|
47
|
-
if thread.alive?
|
|
48
|
-
thread.kill
|
|
49
|
-
return Textus::Domain::Outcome::Failed.new(
|
|
50
|
-
error: Textus::UsageError.new(
|
|
51
|
-
"refresh exceeded budget #{budget_ms}ms (no fork available — cooperative cancel)",
|
|
52
|
-
),
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
if result.is_a?(Textus::Error)
|
|
57
|
-
Textus::Domain::Outcome::Failed.new(error: result)
|
|
58
|
-
else
|
|
59
|
-
Textus::Domain::Outcome::Refreshed.new(envelope: result)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def run_timed_with_fork(budget_ms, key)
|
|
64
|
-
result = nil
|
|
65
|
-
thread = Thread.new do
|
|
66
|
-
result = @worker.run(key)
|
|
67
|
-
rescue Textus::Error => e
|
|
68
|
-
result = e
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
thread.join(budget_ms / 1000.0)
|
|
72
|
-
|
|
73
|
-
if thread.alive?
|
|
74
|
-
thread.kill
|
|
75
|
-
|
|
76
|
-
# Single-flight: if a sibling process / earlier fork holds the
|
|
77
|
-
# per-leaf lock, don't fork another worker — they're already
|
|
78
|
-
# doing this work.
|
|
79
|
-
probe = Textus::Infra::Refresh::Lock.new(root: @store_root, key: key)
|
|
80
|
-
return Textus::Domain::Outcome::Detached.new unless probe.try_acquire
|
|
81
|
-
|
|
82
|
-
probe.release
|
|
83
|
-
|
|
84
|
-
payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
|
|
85
|
-
payload[:ctx] = @hook_context if @hook_context
|
|
86
|
-
@events.publish(:refresh_backgrounded, **payload)
|
|
87
|
-
@detached_spawner.call(store_root: @store_root, key: key)
|
|
88
|
-
Textus::Domain::Outcome::Detached.new
|
|
89
|
-
elsif result.is_a?(Textus::Error)
|
|
90
|
-
Textus::Domain::Outcome::Failed.new(error: result)
|
|
91
|
-
else
|
|
92
|
-
Textus::Domain::Outcome::Refreshed.new(envelope: result)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def default_spawner
|
|
97
|
-
Textus::Infra::Refresh::Detached.method(:spawn)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
require "timeout"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Application
|
|
5
|
-
module Write
|
|
6
|
-
module RefreshWorker
|
|
7
|
-
FETCH_TIMEOUT_SECONDS = 30
|
|
8
|
-
|
|
9
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
10
|
-
Impl.new(
|
|
11
|
-
ctx: ctx, caps: caps,
|
|
12
|
-
rpc: session.rpc,
|
|
13
|
-
writer: session.envelope_writer,
|
|
14
|
-
hook_context: session.hook_context
|
|
15
|
-
).call(*, **)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
class Impl
|
|
19
|
-
def initialize(ctx:, caps:, rpc:, writer:, hook_context:)
|
|
20
|
-
@ctx = ctx
|
|
21
|
-
@caps = caps
|
|
22
|
-
@manifest = caps.manifest
|
|
23
|
-
@writer = writer
|
|
24
|
-
@events = caps.events
|
|
25
|
-
@rpc = rpc
|
|
26
|
-
@authorizer = caps.authorizer
|
|
27
|
-
@hook_context = hook_context
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# call(key) is the primary entry; run is kept as an alias for
|
|
31
|
-
# Orchestrator and RefreshAll which call worker.run(key).
|
|
32
|
-
def call(key)
|
|
33
|
-
run(key)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def run(key)
|
|
37
|
-
res = @manifest.resolver.resolve(key)
|
|
38
|
-
mentry = res.entry
|
|
39
|
-
path = res.path
|
|
40
|
-
remaining = res.remaining
|
|
41
|
-
raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
42
|
-
|
|
43
|
-
before_etag = File.exist?(path) ? Etag.for_file(path) : nil
|
|
44
|
-
result = fetch_with_events(key, mentry, remaining)
|
|
45
|
-
persist_and_notify(key, mentry, result, before_etag)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def fetch_timeout_for(key)
|
|
51
|
-
rule = @manifest.rules.for(key)
|
|
52
|
-
rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def fetch_with_events(key, mentry, remaining)
|
|
56
|
-
@events.publish(:refresh_started, ctx: @hook_context, key: key, mode: :sync)
|
|
57
|
-
call_intake(key, mentry, remaining)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def call_intake(key, mentry, remaining)
|
|
61
|
-
timeout = fetch_timeout_for(key)
|
|
62
|
-
Timeout.timeout(timeout) do
|
|
63
|
-
@rpc.invoke(:resolve_intake, mentry.handler,
|
|
64
|
-
caps: @caps,
|
|
65
|
-
config: mentry.config,
|
|
66
|
-
args: { trigger_key: key, leaf_segments: remaining || [] })
|
|
67
|
-
end
|
|
68
|
-
rescue Timeout::Error
|
|
69
|
-
@events.publish(:refresh_failed, ctx: @hook_context, key: key,
|
|
70
|
-
error_class: "Timeout::Error",
|
|
71
|
-
error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
|
|
72
|
-
raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
|
|
73
|
-
rescue Textus::Error => e
|
|
74
|
-
@events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
|
|
75
|
-
error_message: e.message)
|
|
76
|
-
raise
|
|
77
|
-
rescue StandardError => e
|
|
78
|
-
@events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
|
|
79
|
-
error_message: e.message)
|
|
80
|
-
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def persist_and_notify(key, mentry, result, before_etag)
|
|
84
|
-
normalized = RefreshWorker.send(:normalize_action_result, result, format: mentry.format)
|
|
85
|
-
@authorizer.authorize_write!(mentry, role: @ctx.role)
|
|
86
|
-
envelope = @writer.put(
|
|
87
|
-
key,
|
|
88
|
-
mentry: mentry,
|
|
89
|
-
payload: Textus::Application::Envelope::Writer::Payload.new(
|
|
90
|
-
meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
|
|
91
|
-
),
|
|
92
|
-
)
|
|
93
|
-
change = detect_change(before_etag, envelope)
|
|
94
|
-
@events.publish(:entry_refreshed, ctx: @hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
|
|
95
|
-
envelope
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def detect_change(before_etag, envelope)
|
|
99
|
-
if before_etag.nil? then :created
|
|
100
|
-
elsif envelope.etag == before_etag then :unchanged
|
|
101
|
-
else :updated
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def self.normalize_action_result(res, format:)
|
|
107
|
-
res = res.transform_keys(&:to_s) if res.is_a?(Hash)
|
|
108
|
-
res ||= {}
|
|
109
|
-
meta_val = res["_meta"]
|
|
110
|
-
body = res["body"]
|
|
111
|
-
content = res["content"]
|
|
112
|
-
|
|
113
|
-
case format
|
|
114
|
-
when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
|
|
115
|
-
when "text" then { meta: {}, body: body.to_s, content: nil }
|
|
116
|
-
when "json", "yaml"
|
|
117
|
-
if !content.nil?
|
|
118
|
-
{ meta: meta_val || {}, body: nil, content: content }
|
|
119
|
-
elsif !body.nil?
|
|
120
|
-
{ meta: {}, body: body.to_s, content: nil }
|
|
121
|
-
else
|
|
122
|
-
raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
|
|
123
|
-
end
|
|
124
|
-
else
|
|
125
|
-
raise Textus::UsageError.new("unknown format #{format.inspect}")
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
private_class_method :normalize_action_result
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
Textus::Application::UseCase.register(:refresh, Textus::Application::Write::RefreshWorker, caps: :write)
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
require_relative "authority_gate"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Application
|
|
5
|
-
module Write
|
|
6
|
-
module Reject
|
|
7
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
8
|
-
Impl.new(
|
|
9
|
-
ctx: ctx, caps: caps,
|
|
10
|
-
writer: session.envelope_writer,
|
|
11
|
-
hook_context: session.hook_context
|
|
12
|
-
).call(*, **)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
class Impl
|
|
16
|
-
include AuthorityGate
|
|
17
|
-
|
|
18
|
-
def initialize(ctx:, caps:, writer:, hook_context:)
|
|
19
|
-
@ctx = ctx
|
|
20
|
-
@caps = caps
|
|
21
|
-
@manifest = caps.manifest
|
|
22
|
-
@writer = writer
|
|
23
|
-
@events = caps.events
|
|
24
|
-
@authorizer = caps.authorizer
|
|
25
|
-
@hook_context = hook_context
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(pending_key)
|
|
29
|
-
assert_accept_authority!("reject")
|
|
30
|
-
|
|
31
|
-
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
32
|
-
unless mentry.in_proposal_zone?
|
|
33
|
-
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
env = Textus::Application::Read::Get::Impl.new(
|
|
37
|
-
ctx: @ctx, caps: @caps,
|
|
38
|
-
).call(pending_key)
|
|
39
|
-
proposal = env.meta&.dig("proposal") or
|
|
40
|
-
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
41
|
-
target_key = proposal["target_key"] or
|
|
42
|
-
raise ProposalError.new("proposal missing target_key")
|
|
43
|
-
|
|
44
|
-
Textus::Application::Write::Delete::Impl.new(
|
|
45
|
-
ctx: @ctx, caps: @caps, writer: @writer,
|
|
46
|
-
hook_context: @hook_context
|
|
47
|
-
).call(pending_key, suppress_events: true)
|
|
48
|
-
|
|
49
|
-
@events.publish(:proposal_rejected,
|
|
50
|
-
ctx: @hook_context,
|
|
51
|
-
key: pending_key,
|
|
52
|
-
target_key: target_key)
|
|
53
|
-
|
|
54
|
-
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
Textus::Application::UseCase.register(:reject, Textus::Application::Write::Reject, caps: :write)
|
data/lib/textus/session.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Per-call session. Holds ctx (role, correlation_id, now, dry_run) and
|
|
3
|
-
# the three caps records. Generates one method per registered use case.
|
|
4
|
-
class Session
|
|
5
|
-
attr_reader :ctx, :read_caps, :write_caps, :hook_caps
|
|
6
|
-
|
|
7
|
-
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
8
|
-
read_caps, write_caps, hook_caps = Application.caps_from_store(store)
|
|
9
|
-
new(
|
|
10
|
-
ctx: Application::Context.build(role: role, correlation_id: correlation_id, dry_run: dry_run),
|
|
11
|
-
read_caps: read_caps, write_caps: write_caps, hook_caps: hook_caps
|
|
12
|
-
)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def initialize(ctx:, read_caps:, write_caps:, hook_caps:)
|
|
16
|
-
@ctx = ctx
|
|
17
|
-
@read_caps = read_caps
|
|
18
|
-
@write_caps = write_caps
|
|
19
|
-
@hook_caps = hook_caps
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def with_role(role)
|
|
23
|
-
self.class.new(
|
|
24
|
-
ctx: @ctx.with_role(role),
|
|
25
|
-
read_caps: @read_caps, write_caps: @write_caps, hook_caps: @hook_caps
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def hook_context
|
|
30
|
-
@hook_context ||= Hooks::Context.new(session: self)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def rpc = @hook_caps.rpc
|
|
34
|
-
def events = @hook_caps.events
|
|
35
|
-
|
|
36
|
-
def envelope_reader
|
|
37
|
-
@envelope_reader ||= Application::Envelope::Reader.new(
|
|
38
|
-
file_store: @read_caps.file_store, manifest: @read_caps.manifest,
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def envelope_writer
|
|
43
|
-
@envelope_writer ||= Application::Envelope::Writer.new(
|
|
44
|
-
file_store: @write_caps.file_store, manifest: @write_caps.manifest,
|
|
45
|
-
schemas: @write_caps.schemas, audit_log: @write_caps.audit_log,
|
|
46
|
-
ctx: @ctx, reader: envelope_reader
|
|
47
|
-
)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def boot(...) = Textus::Boot.run(self, ...)
|
|
51
|
-
def doctor(...) = Textus::Doctor.run(self, ...)
|
|
52
|
-
|
|
53
|
-
def refresh_orchestrator
|
|
54
|
-
@refresh_orchestrator ||= Application::Write::RefreshOrchestrator.new(
|
|
55
|
-
worker: refresh_worker,
|
|
56
|
-
store_root: @write_caps.root,
|
|
57
|
-
events: @write_caps.events,
|
|
58
|
-
ctx: @ctx,
|
|
59
|
-
hook_context: hook_context,
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def refresh_worker
|
|
64
|
-
@refresh_worker ||= Application::Write::RefreshWorker::Impl.new(
|
|
65
|
-
ctx: @ctx, caps: @write_caps,
|
|
66
|
-
rpc: rpc, writer: envelope_writer, hook_context: hook_context
|
|
67
|
-
)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Generated dispatch methods. Defined AFTER all use-cases have registered
|
|
71
|
-
# (Zeitwerk.eager_load runs in lib/textus.rb, then session.rb is explicitly
|
|
72
|
-
# required so UseCase.entries is fully populated).
|
|
73
|
-
Application::UseCase.each do |entry|
|
|
74
|
-
verb = entry.verb
|
|
75
|
-
mod = entry.mod
|
|
76
|
-
caps_sym = entry.caps_kind
|
|
77
|
-
|
|
78
|
-
define_method(verb) do |*args, **kwargs|
|
|
79
|
-
fixed = { session: self, ctx: @ctx, caps: caps_sym == :read ? @read_caps : @write_caps }
|
|
80
|
-
mod.call(*args, **fixed, **kwargs)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|