@pi-in-go/pigpen-ahp 0.1.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.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
package ahp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"sync"
|
|
9
|
+
"sync/atomic"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// ─── Configuration ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
// Config tunes a [Client].
|
|
18
|
+
type Config struct {
|
|
19
|
+
// DefaultRequestTimeout bounds how long a [Client.Request] waits
|
|
20
|
+
// for a response. Zero disables the timeout.
|
|
21
|
+
DefaultRequestTimeout time.Duration
|
|
22
|
+
|
|
23
|
+
// SubscriptionBuffer is the size of each subscription's event
|
|
24
|
+
// channel. Slow consumers that fail to drain within this many
|
|
25
|
+
// messages will drop frames and must resubscribe.
|
|
26
|
+
SubscriptionBuffer int
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// DefaultConfig returns a [Config] with sensible defaults: 30s request
|
|
30
|
+
// timeout, 256-message subscription buffer.
|
|
31
|
+
func DefaultConfig() Config {
|
|
32
|
+
return Config{
|
|
33
|
+
DefaultRequestTimeout: 30 * time.Second,
|
|
34
|
+
SubscriptionBuffer: 256,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Subscription events ─────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
// SubscriptionEvent is the marker interface implemented by every
|
|
41
|
+
// subscription event variant.
|
|
42
|
+
type SubscriptionEvent interface{ isSubscriptionEvent() }
|
|
43
|
+
|
|
44
|
+
// SubscriptionEventAction is a write-ahead action envelope for the
|
|
45
|
+
// subscription's channel.
|
|
46
|
+
type SubscriptionEventAction struct {
|
|
47
|
+
Envelope ahptypes.ActionEnvelope
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func (SubscriptionEventAction) isSubscriptionEvent() {}
|
|
51
|
+
|
|
52
|
+
// SubscriptionEventSessionAdded mirrors the `root/sessionAdded`
|
|
53
|
+
// notification.
|
|
54
|
+
type SubscriptionEventSessionAdded struct {
|
|
55
|
+
Params ahptypes.SessionAddedParams
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (SubscriptionEventSessionAdded) isSubscriptionEvent() {}
|
|
59
|
+
|
|
60
|
+
// SubscriptionEventSessionRemoved mirrors the `root/sessionRemoved`
|
|
61
|
+
// notification.
|
|
62
|
+
type SubscriptionEventSessionRemoved struct {
|
|
63
|
+
Params ahptypes.SessionRemovedParams
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func (SubscriptionEventSessionRemoved) isSubscriptionEvent() {}
|
|
67
|
+
|
|
68
|
+
// SubscriptionEventSessionSummaryChanged mirrors the
|
|
69
|
+
// `root/sessionSummaryChanged` notification.
|
|
70
|
+
type SubscriptionEventSessionSummaryChanged struct {
|
|
71
|
+
Params ahptypes.SessionSummaryChangedParams
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func (SubscriptionEventSessionSummaryChanged) isSubscriptionEvent() {}
|
|
75
|
+
|
|
76
|
+
// SubscriptionEventAuthRequired mirrors the `auth/required`
|
|
77
|
+
// notification.
|
|
78
|
+
type SubscriptionEventAuthRequired struct {
|
|
79
|
+
Params ahptypes.AuthRequiredParams
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (SubscriptionEventAuthRequired) isSubscriptionEvent() {}
|
|
83
|
+
|
|
84
|
+
// ClientEvent is a [SubscriptionEvent] tagged with the channel URI it
|
|
85
|
+
// was scoped to. Returned by [Client.Events].
|
|
86
|
+
type ClientEvent struct {
|
|
87
|
+
Channel ahptypes.URI
|
|
88
|
+
Event SubscriptionEvent
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Subscription handle ────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
// Subscription is the per-URI fan-out handle returned by
|
|
94
|
+
// [Client.Subscribe] and [Client.AttachSubscription]. Drop the handle
|
|
95
|
+
// by letting [Client.Shutdown] tear it down, or call its own
|
|
96
|
+
// [Subscription.Close] to stop receiving events.
|
|
97
|
+
type Subscription struct {
|
|
98
|
+
uri string
|
|
99
|
+
events chan SubscriptionEvent
|
|
100
|
+
closeMu sync.Mutex
|
|
101
|
+
closed bool
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// URI returns the channel URI this subscription is bound to.
|
|
105
|
+
func (s *Subscription) URI() string { return s.uri }
|
|
106
|
+
|
|
107
|
+
// Events returns a receive-only channel of events. The channel closes
|
|
108
|
+
// when the [Client] shuts down or the caller invokes
|
|
109
|
+
// [Subscription.Close].
|
|
110
|
+
func (s *Subscription) Events() <-chan SubscriptionEvent { return s.events }
|
|
111
|
+
|
|
112
|
+
// Close stops the subscription locally without notifying the server.
|
|
113
|
+
// Safe to call multiple times.
|
|
114
|
+
func (s *Subscription) Close() {
|
|
115
|
+
s.closeMu.Lock()
|
|
116
|
+
defer s.closeMu.Unlock()
|
|
117
|
+
if s.closed {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
s.closed = true
|
|
121
|
+
close(s.events)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func (s *Subscription) trySend(ev SubscriptionEvent) {
|
|
125
|
+
s.closeMu.Lock()
|
|
126
|
+
defer s.closeMu.Unlock()
|
|
127
|
+
if s.closed {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
select {
|
|
131
|
+
case s.events <- ev:
|
|
132
|
+
default:
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── DispatchHandle ─────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
// DispatchHandle is the receipt returned by [Client.Dispatch],
|
|
139
|
+
// recording the client-assigned sequence number for the dispatched
|
|
140
|
+
// action.
|
|
141
|
+
type DispatchHandle struct {
|
|
142
|
+
ClientSeq int64
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Client ─────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
// pendingResult routes a single in-flight request's outcome to its
|
|
148
|
+
// waiter.
|
|
149
|
+
type pendingResult struct {
|
|
150
|
+
value json.RawMessage
|
|
151
|
+
err *ahptypes.JsonRpcError
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// outboundMsg is the writer goroutine's input queue payload.
|
|
155
|
+
type outboundMsg struct {
|
|
156
|
+
msg ahptypes.JsonRpcMessage
|
|
157
|
+
// done, if non-nil, receives the result of the send attempt.
|
|
158
|
+
done chan error
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// EventStream is a top-level fan-in receiver over every inbound event
|
|
162
|
+
// from a [Client]. Returned by [Client.Events].
|
|
163
|
+
type EventStream struct {
|
|
164
|
+
events chan ClientEvent
|
|
165
|
+
closeMu sync.Mutex
|
|
166
|
+
closed bool
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Events returns a receive-only channel of every [ClientEvent].
|
|
170
|
+
func (s *EventStream) Events() <-chan ClientEvent { return s.events }
|
|
171
|
+
|
|
172
|
+
// Close stops the stream. Safe to call multiple times.
|
|
173
|
+
func (s *EventStream) Close() {
|
|
174
|
+
s.closeMu.Lock()
|
|
175
|
+
defer s.closeMu.Unlock()
|
|
176
|
+
if s.closed {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
s.closed = true
|
|
180
|
+
close(s.events)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func (s *EventStream) trySend(ev ClientEvent) {
|
|
184
|
+
s.closeMu.Lock()
|
|
185
|
+
defer s.closeMu.Unlock()
|
|
186
|
+
if s.closed {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
select {
|
|
190
|
+
case s.events <- ev:
|
|
191
|
+
default:
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ServerRequestHandler answers server-initiated JSON-RPC requests.
|
|
196
|
+
//
|
|
197
|
+
// Return a JSON-encoded result on success, or a non-nil JSON-RPC error
|
|
198
|
+
// to send an error response.
|
|
199
|
+
type ServerRequestHandler func(method string, params json.RawMessage) (json.RawMessage, *ahptypes.JsonRpcError)
|
|
200
|
+
|
|
201
|
+
// ResourceRequestHandlers is a typed registry for symmetrical resource requests.
|
|
202
|
+
//
|
|
203
|
+
// Install it with [Client.SetResourceRequestHandlers]. A nil per-method
|
|
204
|
+
// handler is reported as MethodNotFound.
|
|
205
|
+
type ResourceRequestHandlers struct {
|
|
206
|
+
OnResourceRead func(context.Context, ahptypes.ResourceReadParams) (*ahptypes.ResourceReadResult, error)
|
|
207
|
+
OnResourceWrite func(context.Context, ahptypes.ResourceWriteParams) (*ahptypes.ResourceWriteResult, error)
|
|
208
|
+
OnResourceList func(context.Context, ahptypes.ResourceListParams) (*ahptypes.ResourceListResult, error)
|
|
209
|
+
OnResourceCopy func(context.Context, ahptypes.ResourceCopyParams) (*ahptypes.ResourceCopyResult, error)
|
|
210
|
+
OnResourceDelete func(context.Context, ahptypes.ResourceDeleteParams) (*ahptypes.ResourceDeleteResult, error)
|
|
211
|
+
OnResourceMove func(context.Context, ahptypes.ResourceMoveParams) (*ahptypes.ResourceMoveResult, error)
|
|
212
|
+
OnResourceResolve func(context.Context, ahptypes.ResourceResolveParams) (*ahptypes.ResourceResolveResult, error)
|
|
213
|
+
OnResourceMkdir func(context.Context, ahptypes.ResourceMkdirParams) (*ahptypes.ResourceMkdirResult, error)
|
|
214
|
+
OnResourceRequest func(context.Context, ahptypes.ResourceRequestParams) (*ahptypes.ResourceRequestResult, error)
|
|
215
|
+
OnCreateResourceWatch func(context.Context, ahptypes.CreateResourceWatchParams) (*ahptypes.CreateResourceWatchResult, error)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Client is an async JSON-RPC client driving a pluggable [Transport].
|
|
219
|
+
//
|
|
220
|
+
// A Client is created with [Connect] which spawns a background
|
|
221
|
+
// goroutine that pumps inbound frames and dispatches outbound
|
|
222
|
+
// requests. Methods are safe to call from multiple goroutines.
|
|
223
|
+
type Client struct {
|
|
224
|
+
cfg Config
|
|
225
|
+
transport Transport
|
|
226
|
+
|
|
227
|
+
// outbound carries messages to be sent to the writer goroutine.
|
|
228
|
+
outbound chan outboundMsg
|
|
229
|
+
|
|
230
|
+
// pending is the request-correlation map keyed by JSON-RPC id.
|
|
231
|
+
pendingMu sync.Mutex
|
|
232
|
+
pending map[uint64]chan pendingResult
|
|
233
|
+
|
|
234
|
+
// subscriptionsMu guards subscriptions and the all-events
|
|
235
|
+
// fan-out registry.
|
|
236
|
+
subscriptionsMu sync.Mutex
|
|
237
|
+
subscriptions map[string][]*Subscription
|
|
238
|
+
eventListeners []*EventStream
|
|
239
|
+
|
|
240
|
+
serverRequestMu sync.Mutex
|
|
241
|
+
serverRequestHandler ServerRequestHandler
|
|
242
|
+
|
|
243
|
+
nextID atomic.Uint64
|
|
244
|
+
nextClientSeq atomic.Int64
|
|
245
|
+
|
|
246
|
+
// done closes once the client has begun teardown. Subsequent
|
|
247
|
+
// sends fail with [ErrShutdown].
|
|
248
|
+
done chan struct{}
|
|
249
|
+
// closeOnce guards the centralized close path so concurrent
|
|
250
|
+
// shutdowns / transport faults are idempotent.
|
|
251
|
+
closeOnce sync.Once
|
|
252
|
+
closeErr atomic.Value // holds error
|
|
253
|
+
wg sync.WaitGroup
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Connect / Shutdown ─────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
// Connect wires a [Transport] up to a new [Client] and starts the
|
|
259
|
+
// background reader/writer goroutines.
|
|
260
|
+
//
|
|
261
|
+
// The Client owns the transport from this point: callers should not
|
|
262
|
+
// invoke [Transport.Send] / [Transport.Recv] directly.
|
|
263
|
+
func Connect(_ context.Context, transport Transport, cfg Config) (*Client, error) {
|
|
264
|
+
if cfg.SubscriptionBuffer <= 0 {
|
|
265
|
+
cfg.SubscriptionBuffer = 256
|
|
266
|
+
}
|
|
267
|
+
c := &Client{
|
|
268
|
+
cfg: cfg,
|
|
269
|
+
transport: transport,
|
|
270
|
+
outbound: make(chan outboundMsg, 64),
|
|
271
|
+
pending: make(map[uint64]chan pendingResult),
|
|
272
|
+
subscriptions: make(map[string][]*Subscription),
|
|
273
|
+
done: make(chan struct{}),
|
|
274
|
+
}
|
|
275
|
+
c.nextID.Store(1)
|
|
276
|
+
c.nextClientSeq.Store(1)
|
|
277
|
+
c.wg.Add(2)
|
|
278
|
+
go c.runReader()
|
|
279
|
+
go c.runWriter()
|
|
280
|
+
return c, nil
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Done returns a channel that closes when the client has begun
|
|
284
|
+
// teardown (either via [Client.Shutdown] or a transport failure
|
|
285
|
+
// observed by the background reader/writer).
|
|
286
|
+
func (c *Client) Done() <-chan struct{} { return c.done }
|
|
287
|
+
|
|
288
|
+
// Err returns the first error that triggered teardown, or nil if
|
|
289
|
+
// the client is still running or was shut down cleanly.
|
|
290
|
+
func (c *Client) Err() error {
|
|
291
|
+
if v := c.closeErr.Load(); v != nil {
|
|
292
|
+
if e, ok := v.(error); ok {
|
|
293
|
+
return e
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return nil
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Shutdown gracefully tears down the client. In-flight requests
|
|
300
|
+
// resolve with [ErrShutdown]. Subscriptions and event streams are
|
|
301
|
+
// closed. The underlying transport is closed too.
|
|
302
|
+
//
|
|
303
|
+
// Safe to call multiple times.
|
|
304
|
+
func (c *Client) Shutdown(ctx context.Context) error {
|
|
305
|
+
c.shutdownWithError(nil)
|
|
306
|
+
doneCh := make(chan struct{})
|
|
307
|
+
go func() { c.wg.Wait(); close(doneCh) }()
|
|
308
|
+
select {
|
|
309
|
+
case <-doneCh:
|
|
310
|
+
return nil
|
|
311
|
+
case <-ctx.Done():
|
|
312
|
+
return ctx.Err()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// shutdownWithError runs the centralized close path exactly once.
|
|
317
|
+
// Reader, writer, and Shutdown all funnel through it so concurrent
|
|
318
|
+
// callers can't race on closing channels.
|
|
319
|
+
func (c *Client) shutdownWithError(err error) {
|
|
320
|
+
c.closeOnce.Do(func() {
|
|
321
|
+
if err != nil {
|
|
322
|
+
c.closeErr.Store(err)
|
|
323
|
+
}
|
|
324
|
+
close(c.done)
|
|
325
|
+
|
|
326
|
+
// Close the transport so any blocked Recv unblocks.
|
|
327
|
+
closeCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
328
|
+
_ = c.transport.Close(closeCtx)
|
|
329
|
+
cancel()
|
|
330
|
+
|
|
331
|
+
// Fail every in-flight request.
|
|
332
|
+
failErr := &ahptypes.JsonRpcError{Code: -32000, Message: "client shut down"}
|
|
333
|
+
if err != nil {
|
|
334
|
+
failErr.Message = fmt.Sprintf("client shut down: %v", err)
|
|
335
|
+
}
|
|
336
|
+
c.pendingMu.Lock()
|
|
337
|
+
for id, ch := range c.pending {
|
|
338
|
+
select {
|
|
339
|
+
case ch <- pendingResult{err: failErr}:
|
|
340
|
+
default:
|
|
341
|
+
}
|
|
342
|
+
close(ch)
|
|
343
|
+
delete(c.pending, id)
|
|
344
|
+
}
|
|
345
|
+
c.pendingMu.Unlock()
|
|
346
|
+
|
|
347
|
+
// Close every subscription and listener so consumers see EOF.
|
|
348
|
+
c.subscriptionsMu.Lock()
|
|
349
|
+
subs := c.subscriptions
|
|
350
|
+
listeners := c.eventListeners
|
|
351
|
+
c.subscriptions = map[string][]*Subscription{}
|
|
352
|
+
c.eventListeners = nil
|
|
353
|
+
c.subscriptionsMu.Unlock()
|
|
354
|
+
for _, list := range subs {
|
|
355
|
+
for _, s := range list {
|
|
356
|
+
s.Close()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for _, l := range listeners {
|
|
360
|
+
l.Close()
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// runWriter drains outbound messages and ships them via the transport.
|
|
366
|
+
// Exits when c.done is closed.
|
|
367
|
+
func (c *Client) runWriter() {
|
|
368
|
+
defer c.wg.Done()
|
|
369
|
+
for {
|
|
370
|
+
select {
|
|
371
|
+
case <-c.done:
|
|
372
|
+
return
|
|
373
|
+
case msg, ok := <-c.outbound:
|
|
374
|
+
if !ok {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
wire, err := EncodeMessage(msg.msg)
|
|
378
|
+
if err != nil {
|
|
379
|
+
if msg.done != nil {
|
|
380
|
+
msg.done <- err
|
|
381
|
+
}
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
ctx, cancel := contextWithDone(context.Background(), c.done)
|
|
385
|
+
sendErr := c.transport.Send(ctx, wire)
|
|
386
|
+
cancel()
|
|
387
|
+
if msg.done != nil {
|
|
388
|
+
msg.done <- sendErr
|
|
389
|
+
}
|
|
390
|
+
if sendErr != nil {
|
|
391
|
+
// Transport faulted — trigger global shutdown so all
|
|
392
|
+
// consumers see EOF in bounded time.
|
|
393
|
+
c.shutdownWithError(fmt.Errorf("ahp: transport send: %w", sendErr))
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// runReader pumps inbound messages from the transport and dispatches
|
|
401
|
+
// them to the pending map / subscriptions. Exits when the transport
|
|
402
|
+
// reports closed.
|
|
403
|
+
func (c *Client) runReader() {
|
|
404
|
+
defer c.wg.Done()
|
|
405
|
+
for {
|
|
406
|
+
select {
|
|
407
|
+
case <-c.done:
|
|
408
|
+
return
|
|
409
|
+
default:
|
|
410
|
+
}
|
|
411
|
+
ctx, cancel := contextWithDone(context.Background(), c.done)
|
|
412
|
+
msg, err := c.transport.Recv(ctx)
|
|
413
|
+
cancel()
|
|
414
|
+
if err != nil {
|
|
415
|
+
c.shutdownWithError(fmt.Errorf("ahp: transport recv: %w", err))
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
parsed, perr := msg.IntoParsed()
|
|
419
|
+
if perr != nil {
|
|
420
|
+
// Skip malformed frames; protocol resync is the server's
|
|
421
|
+
// responsibility.
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
c.dispatch(parsed)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// contextWithDone returns a context that's cancelled when either the
|
|
429
|
+
// parent ctx is cancelled or done is closed — without spawning a
|
|
430
|
+
// long-lived goroutine that outlives the call.
|
|
431
|
+
func contextWithDone(parent context.Context, done <-chan struct{}) (context.Context, context.CancelFunc) {
|
|
432
|
+
ctx, cancel := context.WithCancel(parent)
|
|
433
|
+
stop := make(chan struct{})
|
|
434
|
+
go func() {
|
|
435
|
+
select {
|
|
436
|
+
case <-done:
|
|
437
|
+
cancel()
|
|
438
|
+
case <-stop:
|
|
439
|
+
}
|
|
440
|
+
}()
|
|
441
|
+
wrappedCancel := func() {
|
|
442
|
+
close(stop)
|
|
443
|
+
cancel()
|
|
444
|
+
}
|
|
445
|
+
return ctx, wrappedCancel
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// dispatch routes a parsed message to the matching consumer.
|
|
449
|
+
func (c *Client) dispatch(msg ahptypes.JsonRpcMessage) {
|
|
450
|
+
switch {
|
|
451
|
+
case msg.SuccessResponse != nil:
|
|
452
|
+
c.deliver(msg.SuccessResponse.ID, pendingResult{value: msg.SuccessResponse.Result})
|
|
453
|
+
case msg.ErrorResponse != nil:
|
|
454
|
+
c.deliver(msg.ErrorResponse.ID, pendingResult{err: &msg.ErrorResponse.Error})
|
|
455
|
+
case msg.Notification != nil:
|
|
456
|
+
c.handleNotification(*msg.Notification)
|
|
457
|
+
case msg.Request != nil:
|
|
458
|
+
go c.handleServerRequest(*msg.Request)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
func (c *Client) deliver(id uint64, r pendingResult) {
|
|
463
|
+
c.pendingMu.Lock()
|
|
464
|
+
ch, ok := c.pending[id]
|
|
465
|
+
if ok {
|
|
466
|
+
delete(c.pending, id)
|
|
467
|
+
}
|
|
468
|
+
c.pendingMu.Unlock()
|
|
469
|
+
if !ok {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
ch <- r
|
|
473
|
+
close(ch)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func (c *Client) handleNotification(n ahptypes.JsonRpcNotification) {
|
|
477
|
+
switch n.Method {
|
|
478
|
+
case "action":
|
|
479
|
+
var env ahptypes.ActionEnvelope
|
|
480
|
+
if err := json.Unmarshal(n.Params, &env); err != nil {
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
c.fanOut(env.Channel, SubscriptionEventAction{Envelope: env})
|
|
484
|
+
case "root/sessionAdded":
|
|
485
|
+
var p ahptypes.SessionAddedParams
|
|
486
|
+
if err := json.Unmarshal(n.Params, &p); err != nil {
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
c.fanOut(p.Channel, SubscriptionEventSessionAdded{Params: p})
|
|
490
|
+
case "root/sessionRemoved":
|
|
491
|
+
var p ahptypes.SessionRemovedParams
|
|
492
|
+
if err := json.Unmarshal(n.Params, &p); err != nil {
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
c.fanOut(p.Channel, SubscriptionEventSessionRemoved{Params: p})
|
|
496
|
+
case "root/sessionSummaryChanged":
|
|
497
|
+
var p ahptypes.SessionSummaryChangedParams
|
|
498
|
+
if err := json.Unmarshal(n.Params, &p); err != nil {
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
c.fanOut(p.Channel, SubscriptionEventSessionSummaryChanged{Params: p})
|
|
502
|
+
case "auth/required":
|
|
503
|
+
var p ahptypes.AuthRequiredParams
|
|
504
|
+
if err := json.Unmarshal(n.Params, &p); err != nil {
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
c.fanOut(p.Channel, SubscriptionEventAuthRequired{Params: p})
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
func (c *Client) fanOut(channel string, ev SubscriptionEvent) {
|
|
512
|
+
c.subscriptionsMu.Lock()
|
|
513
|
+
subs := append([]*Subscription(nil), c.subscriptions[channel]...)
|
|
514
|
+
listeners := append([]*EventStream(nil), c.eventListeners...)
|
|
515
|
+
c.subscriptionsMu.Unlock()
|
|
516
|
+
for _, s := range subs {
|
|
517
|
+
s.trySend(ev)
|
|
518
|
+
}
|
|
519
|
+
for _, l := range listeners {
|
|
520
|
+
l.trySend(ClientEvent{Channel: channel, Event: ev})
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
func methodNotFound(method string) *ahptypes.JsonRpcError {
|
|
525
|
+
return &ahptypes.JsonRpcError{
|
|
526
|
+
Code: ahptypes.ErrorCodeMethodNotFound,
|
|
527
|
+
Message: fmt.Sprintf("no handler for server method %q", method),
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
func errorFromHandler(err error) *ahptypes.JsonRpcError {
|
|
532
|
+
var rpc *RPCError
|
|
533
|
+
if errors.As(err, &rpc) {
|
|
534
|
+
return &ahptypes.JsonRpcError{Code: rpc.Code, Message: rpc.Message, Data: rpc.Data}
|
|
535
|
+
}
|
|
536
|
+
return &ahptypes.JsonRpcError{Code: ahptypes.ErrorCodeInternalError, Message: err.Error()}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
func runResourceHandler[P any, R any](method string, params json.RawMessage, handler func(context.Context, P) (*R, error)) (json.RawMessage, *ahptypes.JsonRpcError) {
|
|
540
|
+
if handler == nil {
|
|
541
|
+
return nil, methodNotFound(method)
|
|
542
|
+
}
|
|
543
|
+
var decoded P
|
|
544
|
+
if err := json.Unmarshal(params, &decoded); err != nil {
|
|
545
|
+
return nil, &ahptypes.JsonRpcError{Code: ahptypes.ErrorCodeInvalidParams, Message: err.Error()}
|
|
546
|
+
}
|
|
547
|
+
result, err := handler(context.Background(), decoded)
|
|
548
|
+
if err != nil {
|
|
549
|
+
return nil, errorFromHandler(err)
|
|
550
|
+
}
|
|
551
|
+
if result == nil {
|
|
552
|
+
return json.RawMessage("{}"), nil
|
|
553
|
+
}
|
|
554
|
+
raw, err := json.Marshal(result)
|
|
555
|
+
if err != nil {
|
|
556
|
+
return nil, &ahptypes.JsonRpcError{Code: ahptypes.ErrorCodeInternalError, Message: err.Error()}
|
|
557
|
+
}
|
|
558
|
+
return raw, nil
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// SetServerRequestHandler installs the generic handler for server-initiated
|
|
562
|
+
// JSON-RPC requests.
|
|
563
|
+
func (c *Client) SetServerRequestHandler(h ServerRequestHandler) {
|
|
564
|
+
c.serverRequestMu.Lock()
|
|
565
|
+
defer c.serverRequestMu.Unlock()
|
|
566
|
+
c.serverRequestHandler = h
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ClearServerRequestHandler removes the handler for server-initiated requests.
|
|
570
|
+
func (c *Client) ClearServerRequestHandler() {
|
|
571
|
+
c.SetServerRequestHandler(nil)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// SetResourceRequestHandlers installs typed handlers for symmetrical resource
|
|
575
|
+
// requests.
|
|
576
|
+
func (c *Client) SetResourceRequestHandlers(h ResourceRequestHandlers) {
|
|
577
|
+
c.SetServerRequestHandler(func(method string, params json.RawMessage) (json.RawMessage, *ahptypes.JsonRpcError) {
|
|
578
|
+
switch method {
|
|
579
|
+
case "resourceRead":
|
|
580
|
+
return runResourceHandler(method, params, h.OnResourceRead)
|
|
581
|
+
case "resourceWrite":
|
|
582
|
+
return runResourceHandler(method, params, h.OnResourceWrite)
|
|
583
|
+
case "resourceList":
|
|
584
|
+
return runResourceHandler(method, params, h.OnResourceList)
|
|
585
|
+
case "resourceCopy":
|
|
586
|
+
return runResourceHandler(method, params, h.OnResourceCopy)
|
|
587
|
+
case "resourceDelete":
|
|
588
|
+
return runResourceHandler(method, params, h.OnResourceDelete)
|
|
589
|
+
case "resourceMove":
|
|
590
|
+
return runResourceHandler(method, params, h.OnResourceMove)
|
|
591
|
+
case "resourceResolve":
|
|
592
|
+
return runResourceHandler(method, params, h.OnResourceResolve)
|
|
593
|
+
case "resourceMkdir":
|
|
594
|
+
return runResourceHandler(method, params, h.OnResourceMkdir)
|
|
595
|
+
case "resourceRequest":
|
|
596
|
+
return runResourceHandler(method, params, h.OnResourceRequest)
|
|
597
|
+
case "createResourceWatch":
|
|
598
|
+
return runResourceHandler(method, params, h.OnCreateResourceWatch)
|
|
599
|
+
default:
|
|
600
|
+
return nil, methodNotFound(method)
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
func (c *Client) handleServerRequest(req ahptypes.JsonRpcRequest) {
|
|
606
|
+
c.serverRequestMu.Lock()
|
|
607
|
+
handler := c.serverRequestHandler
|
|
608
|
+
c.serverRequestMu.Unlock()
|
|
609
|
+
|
|
610
|
+
var response ahptypes.JsonRpcMessage
|
|
611
|
+
if handler == nil {
|
|
612
|
+
response = ahptypes.JsonRpcMessage{ErrorResponse: &ahptypes.JsonRpcErrorResponse{
|
|
613
|
+
JsonRpc: ahptypes.JsonRpcV2,
|
|
614
|
+
ID: req.ID,
|
|
615
|
+
Error: *methodNotFound(req.Method),
|
|
616
|
+
}}
|
|
617
|
+
} else if result, rpcErr := handler(req.Method, req.Params); rpcErr != nil {
|
|
618
|
+
response = ahptypes.JsonRpcMessage{ErrorResponse: &ahptypes.JsonRpcErrorResponse{
|
|
619
|
+
JsonRpc: ahptypes.JsonRpcV2,
|
|
620
|
+
ID: req.ID,
|
|
621
|
+
Error: *rpcErr,
|
|
622
|
+
}}
|
|
623
|
+
} else {
|
|
624
|
+
if len(result) == 0 {
|
|
625
|
+
result = json.RawMessage("null")
|
|
626
|
+
}
|
|
627
|
+
response = ahptypes.JsonRpcMessage{SuccessResponse: &ahptypes.JsonRpcSuccessResponse{
|
|
628
|
+
JsonRpc: ahptypes.JsonRpcV2,
|
|
629
|
+
ID: req.ID,
|
|
630
|
+
Result: result,
|
|
631
|
+
}}
|
|
632
|
+
}
|
|
633
|
+
_ = c.send(context.Background(), response)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ─── Request / Notify ───────────────────────────────────────────────────
|
|
637
|
+
|
|
638
|
+
// Request sends a JSON-RPC request and decodes the response into out.
|
|
639
|
+
// If out is nil, the result is discarded.
|
|
640
|
+
func (c *Client) Request(ctx context.Context, method string, params any, out any) error {
|
|
641
|
+
select {
|
|
642
|
+
case <-c.done:
|
|
643
|
+
return ErrShutdown
|
|
644
|
+
default:
|
|
645
|
+
}
|
|
646
|
+
rawParams, err := encodeParams(params)
|
|
647
|
+
if err != nil {
|
|
648
|
+
return err
|
|
649
|
+
}
|
|
650
|
+
id := c.nextID.Add(1) - 1
|
|
651
|
+
resultCh := make(chan pendingResult, 1)
|
|
652
|
+
|
|
653
|
+
c.pendingMu.Lock()
|
|
654
|
+
c.pending[id] = resultCh
|
|
655
|
+
c.pendingMu.Unlock()
|
|
656
|
+
|
|
657
|
+
req := ahptypes.JsonRpcMessage{Request: &ahptypes.JsonRpcRequest{
|
|
658
|
+
JsonRpc: ahptypes.JsonRpcV2,
|
|
659
|
+
ID: id,
|
|
660
|
+
Method: method,
|
|
661
|
+
Params: rawParams,
|
|
662
|
+
}}
|
|
663
|
+
if err := c.send(ctx, req); err != nil {
|
|
664
|
+
c.pendingMu.Lock()
|
|
665
|
+
delete(c.pending, id)
|
|
666
|
+
c.pendingMu.Unlock()
|
|
667
|
+
return err
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Apply the configured default timeout if no deadline is set.
|
|
671
|
+
if c.cfg.DefaultRequestTimeout > 0 {
|
|
672
|
+
if _, ok := ctx.Deadline(); !ok {
|
|
673
|
+
var cancel context.CancelFunc
|
|
674
|
+
ctx, cancel = context.WithTimeout(ctx, c.cfg.DefaultRequestTimeout)
|
|
675
|
+
defer cancel()
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
select {
|
|
680
|
+
case r := <-resultCh:
|
|
681
|
+
if r.err != nil {
|
|
682
|
+
return &RPCError{Code: r.err.Code, Message: r.err.Message, Data: r.err.Data}
|
|
683
|
+
}
|
|
684
|
+
if out != nil && len(r.value) > 0 && string(r.value) != "null" {
|
|
685
|
+
if err := json.Unmarshal(r.value, out); err != nil {
|
|
686
|
+
return fmt.Errorf("ahp: decode response for %q: %w", method, err)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return nil
|
|
690
|
+
case <-ctx.Done():
|
|
691
|
+
c.pendingMu.Lock()
|
|
692
|
+
delete(c.pending, id)
|
|
693
|
+
c.pendingMu.Unlock()
|
|
694
|
+
return ctx.Err()
|
|
695
|
+
case <-c.done:
|
|
696
|
+
return ErrShutdown
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Notify sends a JSON-RPC notification (fire-and-forget).
|
|
701
|
+
func (c *Client) Notify(ctx context.Context, method string, params any) error {
|
|
702
|
+
select {
|
|
703
|
+
case <-c.done:
|
|
704
|
+
return ErrShutdown
|
|
705
|
+
default:
|
|
706
|
+
}
|
|
707
|
+
rawParams, err := encodeParams(params)
|
|
708
|
+
if err != nil {
|
|
709
|
+
return err
|
|
710
|
+
}
|
|
711
|
+
msg := ahptypes.JsonRpcMessage{Notification: &ahptypes.JsonRpcNotification{
|
|
712
|
+
JsonRpc: ahptypes.JsonRpcV2,
|
|
713
|
+
Method: method,
|
|
714
|
+
Params: rawParams,
|
|
715
|
+
}}
|
|
716
|
+
return c.send(ctx, msg)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
func (c *Client) send(ctx context.Context, msg ahptypes.JsonRpcMessage) error {
|
|
720
|
+
done := make(chan error, 1)
|
|
721
|
+
select {
|
|
722
|
+
case c.outbound <- outboundMsg{msg: msg, done: done}:
|
|
723
|
+
case <-ctx.Done():
|
|
724
|
+
return ctx.Err()
|
|
725
|
+
case <-c.done:
|
|
726
|
+
return ErrShutdown
|
|
727
|
+
}
|
|
728
|
+
select {
|
|
729
|
+
case err := <-done:
|
|
730
|
+
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
|
|
731
|
+
return err
|
|
732
|
+
}
|
|
733
|
+
return err
|
|
734
|
+
case <-ctx.Done():
|
|
735
|
+
return ctx.Err()
|
|
736
|
+
case <-c.done:
|
|
737
|
+
return ErrShutdown
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
func encodeParams(params any) (json.RawMessage, error) {
|
|
742
|
+
if params == nil {
|
|
743
|
+
return nil, nil
|
|
744
|
+
}
|
|
745
|
+
b, err := json.Marshal(params)
|
|
746
|
+
if err != nil {
|
|
747
|
+
return nil, fmt.Errorf("ahp: encode params: %w", err)
|
|
748
|
+
}
|
|
749
|
+
if string(b) == "null" {
|
|
750
|
+
return nil, nil
|
|
751
|
+
}
|
|
752
|
+
return b, nil
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ─── Protocol surface ───────────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
// Initialize issues the `initialize` handshake.
|
|
758
|
+
//
|
|
759
|
+
// protocolVersions is the list of protocol versions the client is
|
|
760
|
+
// willing to speak, ordered most-preferred-first. Pass
|
|
761
|
+
// [ahptypes.SupportedProtocolVersions]() to advertise every version
|
|
762
|
+
// this generated source knows about. initialSubscriptions may be nil
|
|
763
|
+
// or empty.
|
|
764
|
+
func (c *Client) Initialize(ctx context.Context, clientID string, protocolVersions []string, initialSubscriptions []string) (*ahptypes.InitializeResult, error) {
|
|
765
|
+
params := ahptypes.InitializeParams{
|
|
766
|
+
Channel: ahptypes.RootResourceURI,
|
|
767
|
+
ProtocolVersions: protocolVersions,
|
|
768
|
+
ClientId: clientID,
|
|
769
|
+
}
|
|
770
|
+
if len(initialSubscriptions) > 0 {
|
|
771
|
+
params.InitialSubscriptions = initialSubscriptions
|
|
772
|
+
}
|
|
773
|
+
var out ahptypes.InitializeResult
|
|
774
|
+
if err := c.Request(ctx, "initialize", params, &out); err != nil {
|
|
775
|
+
return nil, err
|
|
776
|
+
}
|
|
777
|
+
return &out, nil
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Reconnect re-establishes a dropped connection with the server's
|
|
781
|
+
// `reconnect` flow.
|
|
782
|
+
func (c *Client) Reconnect(ctx context.Context, clientID string, lastSeenServerSeq int64, subscriptions []string) (*ahptypes.ReconnectResult, error) {
|
|
783
|
+
params := ahptypes.ReconnectParams{
|
|
784
|
+
Channel: ahptypes.RootResourceURI,
|
|
785
|
+
ClientId: clientID,
|
|
786
|
+
LastSeenServerSeq: lastSeenServerSeq,
|
|
787
|
+
Subscriptions: subscriptions,
|
|
788
|
+
}
|
|
789
|
+
var out ahptypes.ReconnectResult
|
|
790
|
+
if err := c.Request(ctx, "reconnect", params, &out); err != nil {
|
|
791
|
+
return nil, err
|
|
792
|
+
}
|
|
793
|
+
return &out, nil
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Ping sends a protocol-level `ping` request to verify the connection is
|
|
797
|
+
// still alive and keep it from being closed by idle-timeout intermediaries
|
|
798
|
+
// (proxies, load balancers, etc.). It is a connection-level command scoped to
|
|
799
|
+
// the root channel and carries no payload in either direction — the response
|
|
800
|
+
// itself is the signal. The server responds regardless of whether Initialize
|
|
801
|
+
// has completed or any subscriptions are held.
|
|
802
|
+
func (c *Client) Ping(ctx context.Context) error {
|
|
803
|
+
params := struct {
|
|
804
|
+
Channel ahptypes.URI `json:"channel"`
|
|
805
|
+
}{Channel: ahptypes.RootResourceURI}
|
|
806
|
+
return c.Request(ctx, "ping", params, nil)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Subscribe sends a `subscribe` request and returns the initial snapshot
|
|
810
|
+
// together with a per-URI [Subscription] handle.
|
|
811
|
+
func (c *Client) Subscribe(ctx context.Context, uri string) (*ahptypes.SubscribeResult, *Subscription, error) {
|
|
812
|
+
return c.SubscribeWithDelivery(ctx, uri, nil)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// SubscribeWithDelivery sends a `subscribe` request with advisory delivery
|
|
816
|
+
// preferences and returns the initial snapshot together with a per-URI
|
|
817
|
+
// [Subscription] handle.
|
|
818
|
+
func (c *Client) SubscribeWithDelivery(ctx context.Context, uri string, delivery *ahptypes.SubscriptionDeliveryOptions) (*ahptypes.SubscribeResult, *Subscription, error) {
|
|
819
|
+
sub := c.AttachSubscription(uri)
|
|
820
|
+
var out ahptypes.SubscribeResult
|
|
821
|
+
if err := c.Request(ctx, "subscribe", ahptypes.SubscribeParams{Channel: uri, Delivery: delivery}, &out); err != nil {
|
|
822
|
+
sub.Close()
|
|
823
|
+
return nil, nil, err
|
|
824
|
+
}
|
|
825
|
+
return &out, sub, nil
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// AttachSubscription returns a local [Subscription] for uri without
|
|
829
|
+
// sending a `subscribe` request. Useful when uri was included in
|
|
830
|
+
// `initialSubscriptions` during [Client.Initialize].
|
|
831
|
+
func (c *Client) AttachSubscription(uri string) *Subscription {
|
|
832
|
+
c.subscriptionsMu.Lock()
|
|
833
|
+
defer c.subscriptionsMu.Unlock()
|
|
834
|
+
sub := &Subscription{uri: uri, events: make(chan SubscriptionEvent, c.cfg.SubscriptionBuffer)}
|
|
835
|
+
c.subscriptions[uri] = append(c.subscriptions[uri], sub)
|
|
836
|
+
return sub
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Unsubscribe sends an `unsubscribe` notification and drops every
|
|
840
|
+
// local [Subscription] for uri.
|
|
841
|
+
func (c *Client) Unsubscribe(ctx context.Context, uri string) error {
|
|
842
|
+
c.subscriptionsMu.Lock()
|
|
843
|
+
subs := c.subscriptions[uri]
|
|
844
|
+
delete(c.subscriptions, uri)
|
|
845
|
+
c.subscriptionsMu.Unlock()
|
|
846
|
+
for _, s := range subs {
|
|
847
|
+
s.Close()
|
|
848
|
+
}
|
|
849
|
+
return c.Notify(ctx, "unsubscribe", ahptypes.UnsubscribeParams{Channel: uri})
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Dispatch fires a write-ahead `dispatchAction` notification with a
|
|
853
|
+
// client-assigned sequence number.
|
|
854
|
+
func (c *Client) Dispatch(ctx context.Context, channel string, action ahptypes.StateAction) (DispatchHandle, error) {
|
|
855
|
+
seq := c.nextClientSeq.Add(1) - 1
|
|
856
|
+
err := c.Notify(ctx, "dispatchAction", ahptypes.DispatchActionParams{
|
|
857
|
+
Channel: channel,
|
|
858
|
+
ClientSeq: seq,
|
|
859
|
+
Action: action,
|
|
860
|
+
})
|
|
861
|
+
if err != nil {
|
|
862
|
+
return DispatchHandle{}, err
|
|
863
|
+
}
|
|
864
|
+
return DispatchHandle{ClientSeq: seq}, nil
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ResourceRead reads a resource's content (`resourceRead`). It targets the
|
|
868
|
+
// root channel; any Channel set on params is overwritten.
|
|
869
|
+
func (c *Client) ResourceRead(ctx context.Context, params ahptypes.ResourceReadParams) (*ahptypes.ResourceReadResult, error) {
|
|
870
|
+
params.Channel = ahptypes.RootResourceURI
|
|
871
|
+
var out ahptypes.ResourceReadResult
|
|
872
|
+
if err := c.Request(ctx, "resourceRead", params, &out); err != nil {
|
|
873
|
+
return nil, err
|
|
874
|
+
}
|
|
875
|
+
return &out, nil
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ResourceWrite writes resource content (`resourceWrite`). It targets the root
|
|
879
|
+
// channel; any Channel set on params is overwritten.
|
|
880
|
+
func (c *Client) ResourceWrite(ctx context.Context, params ahptypes.ResourceWriteParams) (*ahptypes.ResourceWriteResult, error) {
|
|
881
|
+
params.Channel = ahptypes.RootResourceURI
|
|
882
|
+
var out ahptypes.ResourceWriteResult
|
|
883
|
+
if err := c.Request(ctx, "resourceWrite", params, &out); err != nil {
|
|
884
|
+
return nil, err
|
|
885
|
+
}
|
|
886
|
+
return &out, nil
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ResourceList lists directory entries (`resourceList`). It targets the root
|
|
890
|
+
// channel; any Channel set on params is overwritten.
|
|
891
|
+
func (c *Client) ResourceList(ctx context.Context, params ahptypes.ResourceListParams) (*ahptypes.ResourceListResult, error) {
|
|
892
|
+
params.Channel = ahptypes.RootResourceURI
|
|
893
|
+
var out ahptypes.ResourceListResult
|
|
894
|
+
if err := c.Request(ctx, "resourceList", params, &out); err != nil {
|
|
895
|
+
return nil, err
|
|
896
|
+
}
|
|
897
|
+
return &out, nil
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// ResourceCopy copies a resource (`resourceCopy`). It targets the root channel;
|
|
901
|
+
// any Channel set on params is overwritten.
|
|
902
|
+
func (c *Client) ResourceCopy(ctx context.Context, params ahptypes.ResourceCopyParams) (*ahptypes.ResourceCopyResult, error) {
|
|
903
|
+
params.Channel = ahptypes.RootResourceURI
|
|
904
|
+
var out ahptypes.ResourceCopyResult
|
|
905
|
+
if err := c.Request(ctx, "resourceCopy", params, &out); err != nil {
|
|
906
|
+
return nil, err
|
|
907
|
+
}
|
|
908
|
+
return &out, nil
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ResourceDelete deletes a resource (`resourceDelete`). It targets the root
|
|
912
|
+
// channel; any Channel set on params is overwritten.
|
|
913
|
+
func (c *Client) ResourceDelete(ctx context.Context, params ahptypes.ResourceDeleteParams) (*ahptypes.ResourceDeleteResult, error) {
|
|
914
|
+
params.Channel = ahptypes.RootResourceURI
|
|
915
|
+
var out ahptypes.ResourceDeleteResult
|
|
916
|
+
if err := c.Request(ctx, "resourceDelete", params, &out); err != nil {
|
|
917
|
+
return nil, err
|
|
918
|
+
}
|
|
919
|
+
return &out, nil
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ResourceMove moves a resource (`resourceMove`). It targets the root channel;
|
|
923
|
+
// any Channel set on params is overwritten.
|
|
924
|
+
func (c *Client) ResourceMove(ctx context.Context, params ahptypes.ResourceMoveParams) (*ahptypes.ResourceMoveResult, error) {
|
|
925
|
+
params.Channel = ahptypes.RootResourceURI
|
|
926
|
+
var out ahptypes.ResourceMoveResult
|
|
927
|
+
if err := c.Request(ctx, "resourceMove", params, &out); err != nil {
|
|
928
|
+
return nil, err
|
|
929
|
+
}
|
|
930
|
+
return &out, nil
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ResourceResolve resolves resource metadata (`resourceResolve`). It targets
|
|
934
|
+
// the root channel; any Channel set on params is overwritten.
|
|
935
|
+
func (c *Client) ResourceResolve(ctx context.Context, params ahptypes.ResourceResolveParams) (*ahptypes.ResourceResolveResult, error) {
|
|
936
|
+
params.Channel = ahptypes.RootResourceURI
|
|
937
|
+
var out ahptypes.ResourceResolveResult
|
|
938
|
+
if err := c.Request(ctx, "resourceResolve", params, &out); err != nil {
|
|
939
|
+
return nil, err
|
|
940
|
+
}
|
|
941
|
+
return &out, nil
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ResourceMkdir creates a directory (`resourceMkdir`). It targets the root
|
|
945
|
+
// channel; any Channel set on params is overwritten.
|
|
946
|
+
func (c *Client) ResourceMkdir(ctx context.Context, params ahptypes.ResourceMkdirParams) (*ahptypes.ResourceMkdirResult, error) {
|
|
947
|
+
params.Channel = ahptypes.RootResourceURI
|
|
948
|
+
var out ahptypes.ResourceMkdirResult
|
|
949
|
+
if err := c.Request(ctx, "resourceMkdir", params, &out); err != nil {
|
|
950
|
+
return nil, err
|
|
951
|
+
}
|
|
952
|
+
return &out, nil
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ResourceRequest requests resource access (`resourceRequest`). It targets the
|
|
956
|
+
// root channel; any Channel set on params is overwritten.
|
|
957
|
+
func (c *Client) ResourceRequest(ctx context.Context, params ahptypes.ResourceRequestParams) (*ahptypes.ResourceRequestResult, error) {
|
|
958
|
+
params.Channel = ahptypes.RootResourceURI
|
|
959
|
+
var out ahptypes.ResourceRequestResult
|
|
960
|
+
if err := c.Request(ctx, "resourceRequest", params, &out); err != nil {
|
|
961
|
+
return nil, err
|
|
962
|
+
}
|
|
963
|
+
return &out, nil
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// CreateResourceWatch creates a resource watcher (`createResourceWatch`). It
|
|
967
|
+
// targets the root channel; any Channel set on params is overwritten.
|
|
968
|
+
func (c *Client) CreateResourceWatch(ctx context.Context, params ahptypes.CreateResourceWatchParams) (*ahptypes.CreateResourceWatchResult, error) {
|
|
969
|
+
params.Channel = ahptypes.RootResourceURI
|
|
970
|
+
var out ahptypes.CreateResourceWatchResult
|
|
971
|
+
if err := c.Request(ctx, "createResourceWatch", params, &out); err != nil {
|
|
972
|
+
return nil, err
|
|
973
|
+
}
|
|
974
|
+
return &out, nil
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Completions requests inline completion items for a partially-typed input
|
|
978
|
+
// (`completions`), e.g. to power `@`-mention pickers. Unlike the resource
|
|
979
|
+
// wrappers, the caller-supplied Channel (the chat URI the completion is scoped
|
|
980
|
+
// to) is preserved. Debounce calls to avoid flooding the server on every
|
|
981
|
+
// keystroke.
|
|
982
|
+
func (c *Client) Completions(ctx context.Context, params ahptypes.CompletionsParams) (*ahptypes.CompletionsResult, error) {
|
|
983
|
+
var out ahptypes.CompletionsResult
|
|
984
|
+
if err := c.Request(ctx, "completions", params, &out); err != nil {
|
|
985
|
+
return nil, err
|
|
986
|
+
}
|
|
987
|
+
return &out, nil
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// SessionConfigCompletions queries the server for allowed values of a dynamic
|
|
991
|
+
// session config property (`sessionConfigCompletions`). It targets the root
|
|
992
|
+
// channel; any Channel set on params is overwritten.
|
|
993
|
+
func (c *Client) SessionConfigCompletions(ctx context.Context, params ahptypes.SessionConfigCompletionsParams) (*ahptypes.SessionConfigCompletionsResult, error) {
|
|
994
|
+
params.Channel = ahptypes.RootResourceURI
|
|
995
|
+
var out ahptypes.SessionConfigCompletionsResult
|
|
996
|
+
if err := c.Request(ctx, "sessionConfigCompletions", params, &out); err != nil {
|
|
997
|
+
return nil, err
|
|
998
|
+
}
|
|
999
|
+
return &out, nil
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Events returns a new top-level [EventStream] that receives every
|
|
1003
|
+
// inbound event from this client, tagged with the channel URI it was
|
|
1004
|
+
// scoped to. Multiple streams may exist concurrently.
|
|
1005
|
+
func (c *Client) Events() *EventStream {
|
|
1006
|
+
c.subscriptionsMu.Lock()
|
|
1007
|
+
defer c.subscriptionsMu.Unlock()
|
|
1008
|
+
s := &EventStream{events: make(chan ClientEvent, c.cfg.SubscriptionBuffer)}
|
|
1009
|
+
c.eventListeners = append(c.eventListeners, s)
|
|
1010
|
+
return s
|
|
1011
|
+
}
|